Initial commit.
This commit is contained in:
120
Wino.Mail.ViewModels/AboutPageViewModel.cs
Normal file
120
Wino.Mail.ViewModels/AboutPageViewModel.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public class AboutPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IStoreRatingService _storeRatingService;
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IAppInitializerService _appInitializerService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly ILogInitializer _logInitializer;
|
||||
|
||||
public string VersionName => _nativeAppService.GetFullAppVersion();
|
||||
public string DiscordChannelUrl => "https://discord.gg/windows-apps-hub-714581497222398064";
|
||||
public string GitHubUrl => "https://github.com/bkaankose/Wino-Mail/";
|
||||
public string PrivacyPolicyUrl => "https://www.winomail.app/privacy_policy.html";
|
||||
public string PaypalUrl => "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US";
|
||||
|
||||
public AsyncRelayCommand<object> NavigateCommand { get; set; }
|
||||
public AsyncRelayCommand ShareWinoLogCommand { get; set; }
|
||||
public AsyncRelayCommand ShareProtocolLogCommand { get; set; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
public AboutPageViewModel(IStoreRatingService storeRatingService,
|
||||
IDialogService dialogService,
|
||||
INativeAppService nativeAppService,
|
||||
IPreferencesService preferencesService,
|
||||
IAppInitializerService appInitializerService,
|
||||
IFileService fileService,
|
||||
ILogInitializer logInitializer) : base(dialogService)
|
||||
{
|
||||
_storeRatingService = storeRatingService;
|
||||
_nativeAppService = nativeAppService;
|
||||
_logInitializer = logInitializer;
|
||||
_appInitializerService = appInitializerService;
|
||||
_fileService = fileService;
|
||||
|
||||
PreferencesService = preferencesService;
|
||||
NavigateCommand = new AsyncRelayCommand<object>(Navigate);
|
||||
ShareWinoLogCommand = new AsyncRelayCommand(ShareWinoLogAsync);
|
||||
ShareProtocolLogCommand = new AsyncRelayCommand(ShareProtocolLogAsync);
|
||||
}
|
||||
|
||||
protected override void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
PreferencesService.PreferenceChanged -= PreferencesChanged;
|
||||
PreferencesService.PreferenceChanged += PreferencesChanged;
|
||||
}
|
||||
|
||||
protected override void OnDeactivated()
|
||||
{
|
||||
base.OnDeactivated();
|
||||
|
||||
PreferencesService.PreferenceChanged -= PreferencesChanged;
|
||||
}
|
||||
|
||||
private void PreferencesChanged(object sender, string e)
|
||||
{
|
||||
if (e == nameof(PreferencesService.IsLoggingEnabled))
|
||||
{
|
||||
_logInitializer.RefreshLoggingLevel();
|
||||
}
|
||||
}
|
||||
|
||||
private Task ShareProtocolLogAsync()
|
||||
=> SaveLogInternalAsync(ImapTestService.ProtocolLogFileName);
|
||||
private Task ShareWinoLogAsync()
|
||||
=> SaveLogInternalAsync(LogInitializer.WinoLogFileName);
|
||||
|
||||
private async Task SaveLogInternalAsync(string sourceFileName)
|
||||
{
|
||||
var appDataFolder = _appInitializerService.GetApplicationDataFolder();
|
||||
|
||||
var logFile = Path.Combine(appDataFolder, sourceFileName);
|
||||
|
||||
if (!File.Exists(logFile))
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_LogsNotFoundTitle, Translator.Info_LogsNotFoundMessage, Core.Domain.Enums.InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedFolderPath = await DialogService.PickWindowsFolderAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(selectedFolderPath)) return;
|
||||
|
||||
var copiedFilePath = await _fileService.CopyFileAsync(logFile, selectedFolderPath);
|
||||
|
||||
var copiedFileName = Path.GetFileName(copiedFilePath);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_LogsSavedTitle, string.Format(Translator.Info_LogsSavedMessage, copiedFileName), Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
private async Task Navigate(object url)
|
||||
{
|
||||
if (url is string stringUrl)
|
||||
{
|
||||
if (stringUrl == "Store")
|
||||
await ShowRateDialogAsync();
|
||||
else
|
||||
{
|
||||
// Discord disclaimer message about server.
|
||||
if (stringUrl == DiscordChannelUrl)
|
||||
await DialogService.ShowMessageAsync(Translator.DiscordChannelDisclaimerMessage, Translator.DiscordChannelDisclaimerTitle);
|
||||
|
||||
await _nativeAppService.LaunchUriAsync(new Uri(stringUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task ShowRateDialogAsync() => _storeRatingService.LaunchStorePageForReviewAsync();
|
||||
}
|
||||
}
|
||||
158
Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
Normal file
158
Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Messages.Navigation;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class AccountDetailsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IWinoSynchronizerFactory _synchronizerFactory;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public MailAccount Account { get; set; }
|
||||
public ObservableCollection<IMailItemFolder> CurrentFolders { get; set; } = new ObservableCollection<IMailItemFolder>();
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isFocusedInboxEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool areNotificationsEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isAppendMessageSettingVisible;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isAppendMessageSettinEnabled;
|
||||
|
||||
public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null;
|
||||
|
||||
|
||||
public AccountDetailsPageViewModel(IDialogService dialogService,
|
||||
IWinoSynchronizerFactory synchronizerFactory,
|
||||
IAccountService accountService,
|
||||
IFolderService folderService) : base(dialogService)
|
||||
{
|
||||
_synchronizerFactory = synchronizerFactory;
|
||||
_accountService = accountService;
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task SetupSpecialFolders()
|
||||
=> DialogService.HandleSystemFolderConfigurationDialogAsync(Account.Id, _folderService);
|
||||
|
||||
[RelayCommand]
|
||||
private void EditSignature()
|
||||
=> Messenger.Send(new BreadcrumbNavigationRequested("Signature", WinoPage.SignatureManagementPage, 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 async Task RenameAccount()
|
||||
{
|
||||
if (Account == null)
|
||||
return;
|
||||
|
||||
var updatedAccount = await DialogService.ShowEditAccountDialogAsync(Account);
|
||||
|
||||
if (updatedAccount != null)
|
||||
{
|
||||
await _accountService.UpdateAccountAsync(updatedAccount);
|
||||
|
||||
ReportUIChange(new AccountUpdatedMessage(updatedAccount));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAccount()
|
||||
{
|
||||
if (Account == null)
|
||||
return;
|
||||
|
||||
var confirmation = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DeleteAccountConfirmationTitle,
|
||||
string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, Account.Name),
|
||||
Translator.Buttons_Delete);
|
||||
|
||||
if (!confirmation)
|
||||
return;
|
||||
|
||||
await _accountService.DeleteAccountAsync(Account);
|
||||
|
||||
_synchronizerFactory.DeleteSynchronizer(Account);
|
||||
|
||||
// TODO: Clear existing requests.
|
||||
// _synchronizationWorker.ClearRequests(Account.Id);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
|
||||
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters is Guid accountId)
|
||||
{
|
||||
Account = await _accountService.GetAccountAsync(accountId);
|
||||
|
||||
IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
|
||||
AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled;
|
||||
|
||||
IsAppendMessageSettingVisible = Account.ProviderType == MailProviderType.IMAP4;
|
||||
IsAppendMessageSettinEnabled = Account.Preferences.ShouldAppendMessagesToSentFolder;
|
||||
|
||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||
|
||||
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
|
||||
|
||||
foreach (var folder in folderStructures)
|
||||
{
|
||||
CurrentFolders.Add(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
|
||||
if (e.PropertyName == nameof(IsFocusedInboxEnabled) && IsFocusedInboxSupportedForAccount)
|
||||
{
|
||||
Account.Preferences.IsFocusedInboxEnabled = IsFocusedInboxEnabled;
|
||||
|
||||
await _accountService.UpdateAccountAsync(Account);
|
||||
}
|
||||
else if (e.PropertyName == nameof(AreNotificationsEnabled))
|
||||
{
|
||||
Account.Preferences.IsNotificationsEnabled = AreNotificationsEnabled;
|
||||
|
||||
await _accountService.UpdateAccountAsync(Account);
|
||||
}
|
||||
else if (e.PropertyName == nameof(IsAppendMessageSettinEnabled))
|
||||
{
|
||||
Account.Preferences.ShouldAppendMessagesToSentFolder = IsAppendMessageSettinEnabled;
|
||||
|
||||
await _accountService.UpdateAccountAsync(Account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
377
Wino.Mail.ViewModels/AccountManagementViewModel.cs
Normal file
377
Wino.Mail.ViewModels/AccountManagementViewModel.cs
Normal file
@@ -0,0 +1,377 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.AppCenter.Crashes;
|
||||
using Serilog;
|
||||
using Wino.Core;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Store;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Messages.Authorization;
|
||||
using Wino.Core.Messages.Navigation;
|
||||
using Wino.Core.Requests;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class AccountManagementViewModel : BaseViewModel, IRecipient<ProtocolAuthorizationCallbackReceived>
|
||||
{
|
||||
public int FREE_ACCOUNT_COUNT { get; } = 3;
|
||||
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IStoreManagementService _storeManagementService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IAuthenticationProvider _authenticationProvider;
|
||||
private readonly IWinoSynchronizerFactory _synchronizerFactory;
|
||||
|
||||
public ObservableCollection<IAccountProviderDetailViewModel> Accounts { get; set; } = [];
|
||||
|
||||
public bool IsPurchasePanelVisible => !HasUnlimitedAccountProduct;
|
||||
public bool IsAccountCreationAlmostOnLimit => Accounts != null && Accounts.Count == FREE_ACCOUNT_COUNT - 1;
|
||||
public bool HasAccountsDefined => Accounts != null && Accounts.Any();
|
||||
|
||||
public string UsedAccountsString => string.Format(Translator.WinoUpgradeRemainingAccountsMessage, Accounts.Count, FREE_ACCOUNT_COUNT);
|
||||
|
||||
[ObservableProperty]
|
||||
private IAccountProviderDetailViewModel _startupAccount;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
|
||||
private bool hasUnlimitedAccountProduct;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsAccountCreationAlmostOnLimit))]
|
||||
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
|
||||
private bool isAccountCreationBlocked;
|
||||
|
||||
public AccountManagementViewModel(IDialogService dialogService,
|
||||
IWinoNavigationService navigationService,
|
||||
IWinoSynchronizerFactory synchronizerFactory,
|
||||
IAccountService accountService,
|
||||
IProviderService providerService,
|
||||
IFolderService folderService,
|
||||
IStoreManagementService storeManagementService,
|
||||
IPreferencesService preferencesService,
|
||||
IAuthenticationProvider authenticationProvider) : base(dialogService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_synchronizerFactory = synchronizerFactory;
|
||||
_dialogService = dialogService;
|
||||
_providerService = providerService;
|
||||
_folderService = folderService;
|
||||
_storeManagementService = storeManagementService;
|
||||
_preferencesService = preferencesService;
|
||||
_authenticationProvider = authenticationProvider;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void NavigateAccountDetails(AccountProviderDetailViewModel accountDetails)
|
||||
{
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(accountDetails.Account.Name,
|
||||
WinoPage.AccountDetailsPage,
|
||||
accountDetails.Account.Id));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateMergedAccountAsync()
|
||||
{
|
||||
var linkName = await DialogService.ShowTextInputDialogAsync(string.Empty, Translator.DialogMessage_CreateLinkedAccountTitle, Translator.DialogMessage_CreateLinkedAccountMessage);
|
||||
|
||||
if (string.IsNullOrEmpty(linkName)) return;
|
||||
|
||||
// Create arbitary empty merged inbox with an empty Guid and go to edit page.
|
||||
var mergedInbox = new MergedInbox()
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = linkName
|
||||
};
|
||||
|
||||
var mergedAccountProviderDetailViewModel = new MergedAccountProviderDetailViewModel(mergedInbox, new List<AccountProviderDetailViewModel>());
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(mergedAccountProviderDetailViewModel.MergedInbox.Name,
|
||||
WinoPage.MergedAccountDetailsPage,
|
||||
mergedAccountProviderDetailViewModel));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task PurchaseUnlimitedAccountAsync()
|
||||
{
|
||||
var purchaseResult = await _storeManagementService.PurchaseAsync(StoreProductType.UnlimitedAccounts);
|
||||
|
||||
if (purchaseResult == StorePurchaseResult.Succeeded)
|
||||
DialogService.InfoBarMessage(Translator.Info_PurchaseThankYouTitle, Translator.Info_PurchaseThankYouMessage, InfoBarMessageType.Success);
|
||||
else if (purchaseResult == StorePurchaseResult.AlreadyPurchased)
|
||||
DialogService.InfoBarMessage(Translator.Info_PurchaseExistsTitle, Translator.Info_PurchaseExistsMessage, InfoBarMessageType.Warning);
|
||||
|
||||
bool shouldRefreshPurchasePanel = purchaseResult == StorePurchaseResult.Succeeded || purchaseResult == StorePurchaseResult.AlreadyPurchased;
|
||||
|
||||
if (shouldRefreshPurchasePanel)
|
||||
{
|
||||
await ManageStorePurchasesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddNewAccountAsync()
|
||||
{
|
||||
if (IsAccountCreationBlocked)
|
||||
{
|
||||
var isPurchaseClicked = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_AccountLimitMessage, Translator.DialogMessage_AccountLimitTitle, Translator.Buttons_Purchase);
|
||||
|
||||
if (!isPurchaseClicked) return;
|
||||
|
||||
await PurchaseUnlimitedAccountAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MailAccount createdAccount = null;
|
||||
IAccountCreationDialog creationDialog = null;
|
||||
|
||||
try
|
||||
{
|
||||
var providers = _providerService.GetProviderDetails();
|
||||
|
||||
// Select provider.
|
||||
var accountInformationTuple = await _dialogService.ShowNewAccountMailProviderDialogAsync(providers);
|
||||
|
||||
if (accountInformationTuple != null)
|
||||
{
|
||||
creationDialog = _dialogService.GetAccountCreationDialog(accountInformationTuple.Item2);
|
||||
|
||||
var accountName = accountInformationTuple.Item1;
|
||||
var providerType = accountInformationTuple.Item2;
|
||||
|
||||
_accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(providerType);
|
||||
|
||||
CustomServerInformation customServerInformation = null;
|
||||
|
||||
createdAccount = new MailAccount()
|
||||
{
|
||||
ProviderType = providerType,
|
||||
Name = accountName,
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
|
||||
creationDialog.ShowDialog();
|
||||
creationDialog.State = AccountCreationDialogState.SigningIn;
|
||||
|
||||
TokenInformation tokenInformation = null;
|
||||
|
||||
// Custom server implementation requires more async waiting.
|
||||
if (creationDialog is ICustomServerAccountCreationDialog customServerDialog)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For OAuth authentications, we just generate token and assign it to the MailAccount.
|
||||
|
||||
tokenInformation = await _accountService.ExternalAuthenticationAuthenticator.GenerateTokenAsync(createdAccount, false)
|
||||
?? throw new AuthenticationException(Translator.Exception_TokenInfoRetrivalFailed);
|
||||
|
||||
createdAccount.Address = tokenInformation.Address;
|
||||
tokenInformation.AccountId = createdAccount.Id;
|
||||
}
|
||||
|
||||
await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
|
||||
|
||||
// Local account has been created.
|
||||
// Create new synchronizer and start synchronization.
|
||||
|
||||
var synchronizer = _synchronizerFactory.CreateNewSynchronizer(createdAccount);
|
||||
|
||||
if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog)
|
||||
customServerAccountCreationDialog.ShowPreparingFolders();
|
||||
else
|
||||
creationDialog.State = AccountCreationDialogState.PreparingFolders;
|
||||
|
||||
var options = new SynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = SynchronizationType.FoldersOnly
|
||||
};
|
||||
|
||||
var synchronizationResult = await synchronizer.SynchronizeAsync(options);
|
||||
|
||||
if (synchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
||||
|
||||
// Check if Inbox folder is available for the account after synchronization.
|
||||
var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
|
||||
|
||||
if (!isInboxAvailable)
|
||||
throw new Exception(Translator.Exception_InboxNotAvailable);
|
||||
|
||||
// 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 (AccountSetupCanceledException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, WinoErrors.AccountCreation);
|
||||
Crashes.TrackError(ex);
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditMergedAccounts(MergedAccountProviderDetailViewModel mergedAccountProviderDetailViewModel)
|
||||
{
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(mergedAccountProviderDetailViewModel.MergedInbox.Name,
|
||||
WinoPage.MergedAccountDetailsPage,
|
||||
mergedAccountProviderDetailViewModel));
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
Accounts.CollectionChanged -= AccountCollectionChanged;
|
||||
|
||||
PropertyChanged -= PagePropertyChanged;
|
||||
}
|
||||
|
||||
private void AccountCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasAccountsDefined));
|
||||
OnPropertyChanged(nameof(UsedAccountsString));
|
||||
}
|
||||
|
||||
private void PagePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(StartupAccount) && StartupAccount != null)
|
||||
{
|
||||
_preferencesService.StartupEntityId = StartupAccount.StartupEntityId;
|
||||
}
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
Accounts.CollectionChanged -= AccountCollectionChanged;
|
||||
Accounts.CollectionChanged += AccountCollectionChanged;
|
||||
|
||||
await InitializeAccountsAsync();
|
||||
|
||||
PropertyChanged -= PagePropertyChanged;
|
||||
PropertyChanged += PagePropertyChanged;
|
||||
}
|
||||
|
||||
private async Task InitializeAccountsAsync()
|
||||
{
|
||||
StartupAccount = null;
|
||||
|
||||
Accounts.Clear();
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
// Group accounts and display merged ones at the top.
|
||||
var groupedAccounts = accounts.GroupBy(a => a.MergedInboxId);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
foreach (var accountGroup in groupedAccounts)
|
||||
{
|
||||
var mergedInboxId = accountGroup.Key;
|
||||
|
||||
if (mergedInboxId == null)
|
||||
{
|
||||
foreach (var account in accountGroup)
|
||||
{
|
||||
var accountDetails = GetAccountProviderDetails(account);
|
||||
|
||||
Accounts.Add(accountDetails);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var mergedInbox = accountGroup.First(a => a.MergedInboxId == mergedInboxId).MergedInbox;
|
||||
|
||||
var holdingAccountProviderDetails = accountGroup.Select(a => GetAccountProviderDetails(a)).ToList();
|
||||
var mergedAccountViewModel = new MergedAccountProviderDetailViewModel(mergedInbox, holdingAccountProviderDetails);
|
||||
|
||||
Accounts.Add(mergedAccountViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle startup entity.
|
||||
if (_preferencesService.StartupEntityId != null)
|
||||
{
|
||||
StartupAccount = Accounts.FirstOrDefault(a => a.StartupEntityId == _preferencesService.StartupEntityId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await ManageStorePurchasesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ManageStorePurchasesAsync()
|
||||
{
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
HasUnlimitedAccountProduct = await _storeManagementService.HasProductAsync(StoreProductType.UnlimitedAccounts);
|
||||
|
||||
if (!HasUnlimitedAccountProduct)
|
||||
IsAccountCreationBlocked = Accounts.Count >= FREE_ACCOUNT_COUNT;
|
||||
else
|
||||
IsAccountCreationBlocked = false;
|
||||
});
|
||||
}
|
||||
|
||||
private AccountProviderDetailViewModel GetAccountProviderDetails(MailAccount account)
|
||||
{
|
||||
var provider = _providerService.GetProviderDetail(account.ProviderType);
|
||||
|
||||
return new AccountProviderDetailViewModel(provider, account);
|
||||
}
|
||||
|
||||
public void Receive(ProtocolAuthorizationCallbackReceived message)
|
||||
{
|
||||
// Authorization must be completed in account service.
|
||||
|
||||
_accountService.ExternalAuthenticationAuthenticator?.ContinueAuthorization(message.AuthorizationResponseUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
1063
Wino.Mail.ViewModels/AppShellViewModel.cs
Normal file
1063
Wino.Mail.ViewModels/AppShellViewModel.cs
Normal file
File diff suppressed because it is too large
Load Diff
95
Wino.Mail.ViewModels/BaseViewModel.cs
Normal file
95
Wino.Mail.ViewModels/BaseViewModel.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public class BaseViewModel : ObservableRecipient,
|
||||
INavigationAware,
|
||||
IRecipient<AccountCreatedMessage>,
|
||||
IRecipient<AccountRemovedMessage>,
|
||||
IRecipient<AccountUpdatedMessage>,
|
||||
IRecipient<FolderAddedMessage>,
|
||||
IRecipient<FolderUpdatedMessage>,
|
||||
IRecipient<FolderRemovedMessage>,
|
||||
IRecipient<MailAddedMessage>,
|
||||
IRecipient<MailRemovedMessage>,
|
||||
IRecipient<MailUpdatedMessage>,
|
||||
IRecipient<MailDownloadedMessage>,
|
||||
IRecipient<DraftCreated>,
|
||||
IRecipient<DraftFailed>,
|
||||
IRecipient<DraftMapped>
|
||||
{
|
||||
private IDispatcher _dispatcher;
|
||||
public IDispatcher Dispatcher
|
||||
{
|
||||
get
|
||||
{
|
||||
return _dispatcher;
|
||||
}
|
||||
set
|
||||
{
|
||||
_dispatcher = value;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
OnDispatcherAssigned();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected IDialogService DialogService { get; }
|
||||
|
||||
public BaseViewModel(IDialogService dialogService) => DialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||
|
||||
public async Task ExecuteUIThread(Action action) => await Dispatcher?.ExecuteOnUIThread(action);
|
||||
|
||||
public virtual void OnNavigatedTo(NavigationMode mode, object parameters) { IsActive = true; }
|
||||
|
||||
public virtual void OnNavigatedFrom(NavigationMode mode, object parameters) { IsActive = false; }
|
||||
|
||||
protected virtual void OnDispatcherAssigned() { }
|
||||
|
||||
protected virtual void OnMailAdded(MailCopy addedMail) { }
|
||||
protected virtual void OnMailRemoved(MailCopy removedMail) { }
|
||||
protected virtual void OnMailUpdated(MailCopy updatedMail) { }
|
||||
protected virtual void OnMailDownloaded(MailCopy downloadedMail) { }
|
||||
|
||||
protected virtual void OnAccountCreated(MailAccount createdAccount) { }
|
||||
protected virtual void OnAccountRemoved(MailAccount removedAccount) { }
|
||||
protected virtual void OnAccountUpdated(MailAccount updatedAccount) { }
|
||||
|
||||
protected virtual void OnFolderAdded(MailItemFolder addedFolder, MailAccount account) { }
|
||||
protected virtual void OnFolderRemoved(MailItemFolder removedFolder, MailAccount account) { }
|
||||
protected virtual void OnFolderUpdated(MailItemFolder updatedFolder, MailAccount account) { }
|
||||
|
||||
protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { }
|
||||
protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { }
|
||||
protected virtual void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId) { }
|
||||
|
||||
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage
|
||||
=> Messenger.Send(message);
|
||||
|
||||
void IRecipient<AccountCreatedMessage>.Receive(AccountCreatedMessage message) => OnAccountCreated(message.Account);
|
||||
void IRecipient<AccountRemovedMessage>.Receive(AccountRemovedMessage message) => OnAccountRemoved(message.Account);
|
||||
void IRecipient<AccountUpdatedMessage>.Receive(AccountUpdatedMessage message) => OnAccountUpdated(message.Account);
|
||||
|
||||
void IRecipient<FolderAddedMessage>.Receive(FolderAddedMessage message) => OnFolderAdded(message.AddedFolder, message.Account);
|
||||
void IRecipient<FolderUpdatedMessage>.Receive(FolderUpdatedMessage message) => OnFolderUpdated(message.UpdatedFolder, message.Account);
|
||||
void IRecipient<FolderRemovedMessage>.Receive(FolderRemovedMessage message) => OnFolderAdded(message.RemovedFolder, message.Account);
|
||||
|
||||
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<MailDownloadedMessage>.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail);
|
||||
void IRecipient<DraftMapped>.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId);
|
||||
void IRecipient<DraftFailed>.Receive(DraftFailed message) => OnDraftFailed(message.DraftMail, message.Account);
|
||||
void IRecipient<DraftCreated>.Receive(DraftCreated message) => OnDraftCreated(message.DraftMail, message.Account);
|
||||
}
|
||||
}
|
||||
467
Wino.Mail.ViewModels/Collections/WinoMailCollection.cs
Normal file
467
Wino.Mail.ViewModels/Collections/WinoMailCollection.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Collections;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Comparers;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections
|
||||
{
|
||||
public class WinoMailCollection
|
||||
{
|
||||
// We cache each mail copy id for faster access on updates.
|
||||
// If the item provider here for update or removal doesn't exist here
|
||||
// we can ignore the operation.
|
||||
|
||||
public HashSet<Guid> MailCopyIdHashSet = new HashSet<Guid>();
|
||||
|
||||
private ListItemComparer listComparer = new ListItemComparer();
|
||||
|
||||
private readonly ObservableGroupedCollection<object, IMailItem> _mailItemSource = new ObservableGroupedCollection<object, IMailItem>();
|
||||
|
||||
public ReadOnlyObservableGroupedCollection<object, IMailItem> MailItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Property that defines how the item sorting should be done in the collection.
|
||||
/// </summary>
|
||||
public SortingOptionType SortingType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Threading strategy that will help thread items according to the account type.
|
||||
/// </summary>
|
||||
public IThreadingStrategyProvider ThreadingStrategyProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Automatically deletes single mail items after the delete operation or thread->single transition.
|
||||
/// This is useful when reply draft is discarded in the thread. Only enabled for Draft folder for now.
|
||||
/// </summary>
|
||||
public bool PruneSingleNonDraftItems { get; set; }
|
||||
|
||||
public int Count => _mailItemSource.Count;
|
||||
|
||||
public IDispatcher CoreDispatcher { get; set; }
|
||||
|
||||
public WinoMailCollection()
|
||||
{
|
||||
MailItems = new ReadOnlyObservableGroupedCollection<object, IMailItem>(_mailItemSource);
|
||||
}
|
||||
|
||||
public void Clear() => _mailItemSource.Clear();
|
||||
|
||||
private object GetGroupingKey(IMailItem mailItem)
|
||||
{
|
||||
if (SortingType == SortingOptionType.ReceiveDate)
|
||||
return mailItem.CreationDate.ToLocalTime().Date;
|
||||
else
|
||||
return mailItem.FromName;
|
||||
}
|
||||
|
||||
private async Task InsertItemInternalAsync(object groupKey, IMailItem mailItem)
|
||||
=> await ExecuteUIThread(() =>
|
||||
{
|
||||
if (mailItem is MailCopy mailCopy)
|
||||
{
|
||||
MailCopyIdHashSet.Add(mailCopy.UniqueId);
|
||||
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, new MailItemViewModel(mailCopy), listComparer.GetItemComparer());
|
||||
}
|
||||
else if (mailItem is ThreadMailItem threadMailItem)
|
||||
{
|
||||
foreach (var item in threadMailItem.ThreadItems)
|
||||
{
|
||||
MailCopyIdHashSet.Add(item.UniqueId);
|
||||
}
|
||||
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, new ThreadMailItemViewModel(threadMailItem), listComparer.GetItemComparer());
|
||||
}
|
||||
else if (mailItem is MailItemViewModel)
|
||||
{
|
||||
MailCopyIdHashSet.Add(mailItem.UniqueId);
|
||||
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer.GetItemComparer());
|
||||
}
|
||||
});
|
||||
|
||||
private async Task RemoveItemInternalAsync(ObservableGroup<object, IMailItem> group, IMailItem mailItem)
|
||||
{
|
||||
MailCopyIdHashSet.Remove(mailItem.UniqueId);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
group.Remove(mailItem);
|
||||
|
||||
if (group.Count == 0)
|
||||
{
|
||||
_mailItemSource.RemoveGroup(group.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task AddAsync(MailCopy addedItem)
|
||||
{
|
||||
// Check all items for whether this item should be threaded with them.
|
||||
bool shouldExit = false;
|
||||
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
if (shouldExit) break;
|
||||
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
var addedAccountProviderType = addedItem.AssignedAccount.ProviderType;
|
||||
|
||||
var threadingStrategy = ThreadingStrategyProvider.GetStrategy(addedAccountProviderType);
|
||||
|
||||
if (threadingStrategy?.ShouldThreadWithItem(addedItem, item) ?? false)
|
||||
{
|
||||
shouldExit = true;
|
||||
|
||||
if (item is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
{
|
||||
// Item belongs to existing thread.
|
||||
|
||||
/* Add original item to the thread.
|
||||
* If new group key is not the same as existing thread:
|
||||
* -> Remove the whole thread from list
|
||||
* -> Add the thread to the list again for sorting.
|
||||
* Update thread properties.
|
||||
*/
|
||||
|
||||
var existingGroupKey = GetGroupingKey(threadMailItemViewModel);
|
||||
|
||||
threadMailItemViewModel.AddMailItemViewModel(addedItem);
|
||||
|
||||
var newGroupKey = GetGroupingKey(threadMailItemViewModel);
|
||||
|
||||
if (!existingGroupKey.Equals(newGroupKey))
|
||||
{
|
||||
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||
await InsertItemInternalAsync(newGroupKey, threadMailItemViewModel);
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => { threadMailItemViewModel.NotifyPropertyChanges(); });
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Item belongs to a single mail item that is not threaded yet.
|
||||
// Same item might've been tried to added as well.
|
||||
// In that case we must just update the item but not thread it.
|
||||
|
||||
/* Remove target item.
|
||||
* Create a new thread with both items.
|
||||
* Add new thread to the list.
|
||||
*/
|
||||
|
||||
if (item.Id == addedItem.Id)
|
||||
{
|
||||
// Item is already added to the list.
|
||||
// We need to update the copy it holds.
|
||||
|
||||
if (item is MailItemViewModel itemViewModel)
|
||||
{
|
||||
itemViewModel.Update(addedItem);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single item that must be threaded together with added item.
|
||||
|
||||
var threadMailItem = new ThreadMailItem();
|
||||
|
||||
threadMailItem.AddThreadItem(item);
|
||||
threadMailItem.AddThreadItem(addedItem);
|
||||
|
||||
if (threadMailItem.ThreadItems.Count == 1) return;
|
||||
|
||||
var newGroupKey = GetGroupingKey(threadMailItem);
|
||||
|
||||
await RemoveItemInternalAsync(group, item);
|
||||
await InsertItemInternalAsync(newGroupKey, threadMailItem);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update properties.
|
||||
if (item.Id == addedItem.Id && item is MailItemViewModel itemViewModel)
|
||||
{
|
||||
await ExecuteUIThread(() => { itemViewModel.Update(addedItem); });
|
||||
|
||||
shouldExit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldExit)
|
||||
{
|
||||
// At this point all items are already checked and not suitable option was available.
|
||||
// Item doesn't belong to any thread.
|
||||
// Just add it to the collection.
|
||||
|
||||
var groupKey = GetGroupingKey(addedItem);
|
||||
|
||||
await InsertItemInternalAsync(groupKey, addedItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<IMailItem> items, bool clearIdCache)
|
||||
{
|
||||
if (clearIdCache)
|
||||
{
|
||||
MailCopyIdHashSet.Clear();
|
||||
}
|
||||
|
||||
var groupedByName = items
|
||||
.GroupBy(a => GetGroupingKey(a))
|
||||
.Select(a => new ObservableGroup<object, IMailItem>(a.Key, a));
|
||||
|
||||
foreach (var group in groupedByName)
|
||||
{
|
||||
// Store all mail copy ids for faster access.
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (item is MailItemViewModel mailCopyItem && !MailCopyIdHashSet.Contains(item.UniqueId))
|
||||
{
|
||||
MailCopyIdHashSet.Add(item.UniqueId);
|
||||
}
|
||||
else if (item is ThreadMailItemViewModel threadMailItem)
|
||||
{
|
||||
foreach (var mailItem in threadMailItem.ThreadItems)
|
||||
{
|
||||
if (!MailCopyIdHashSet.Contains(mailItem.UniqueId))
|
||||
{
|
||||
MailCopyIdHashSet.Add(mailItem.UniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key);
|
||||
|
||||
if (existingGroup == null)
|
||||
{
|
||||
_mailItemSource.AddGroup(group.Key, group);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
foreach (var item in group)
|
||||
{
|
||||
existingGroup.Add(item);
|
||||
|
||||
// _mailItemSource.InsertItem(existingGroup, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MailItemContainer GetMailItemContainer(Guid uniqueMailId)
|
||||
{
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == uniqueMailId)
|
||||
return new MailItemContainer(singleMailItemViewModel);
|
||||
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(uniqueMailId))
|
||||
{
|
||||
var singleItemViewModel = threadMailItemViewModel.GetItemById(uniqueMailId) as MailItemViewModel;
|
||||
|
||||
return new MailItemContainer(singleItemViewModel, threadMailItemViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fins the item container that updated mail copy belongs to and updates it.
|
||||
/// </summary>
|
||||
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateMailCopy(MailCopy updatedMailCopy)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId))
|
||||
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
|
||||
|
||||
if (itemContainer == null) return;
|
||||
|
||||
// mailCopyIdHashSet.Remove(itemContainer.ItemViewModel.UniqueId);
|
||||
|
||||
itemContainer.ItemViewModel?.Update(updatedMailCopy);
|
||||
|
||||
// mailCopyIdHashSet.Add(updatedMailCopy.UniqueId);
|
||||
|
||||
// Call thread notifications if possible.
|
||||
itemContainer.ThreadViewModel?.NotifyPropertyChanges();
|
||||
});
|
||||
}
|
||||
|
||||
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
||||
{
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId)
|
||||
{
|
||||
if (k + 1 < group.Count)
|
||||
{
|
||||
return group[k + 1] as MailItemViewModel;
|
||||
}
|
||||
else if (i + 1 < groupCount)
|
||||
{
|
||||
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId))
|
||||
{
|
||||
var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel;
|
||||
|
||||
if (singleItemViewModel == null) return null;
|
||||
|
||||
var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel);
|
||||
|
||||
if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count)
|
||||
{
|
||||
return threadMailItemViewModel.ThreadItems[singleItemIndex + 1] as MailItemViewModel;
|
||||
}
|
||||
else if (i + 1 < groupCount)
|
||||
{
|
||||
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(MailCopy removeItem)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.Contains(removeItem.UniqueId)) return;
|
||||
|
||||
// Check all items for whether this item should be threaded with them.
|
||||
bool shouldExit = false;
|
||||
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
if (shouldExit) break;
|
||||
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(removeItem.UniqueId))
|
||||
{
|
||||
var removalItem = threadMailItemViewModel.GetItemById(removeItem.UniqueId);
|
||||
|
||||
if (removalItem == null) return;
|
||||
|
||||
// Threads' Id is equal to the last item they hold.
|
||||
// We can't do Id check here because that'd remove the whole thread.
|
||||
|
||||
/* Remove item from the thread.
|
||||
* If thread had 1 item inside:
|
||||
* -> Remove the thread and insert item as single item.
|
||||
* If thread had 0 item inside:
|
||||
* -> Remove the thread.
|
||||
*/
|
||||
|
||||
await ExecuteUIThread(() => { threadMailItemViewModel.RemoveCopyItem(removalItem); });
|
||||
|
||||
if (threadMailItemViewModel.ThreadItems.Count == 1)
|
||||
{
|
||||
// Convert to single item.
|
||||
|
||||
var singleViewModel = threadMailItemViewModel.GetSingleItemViewModel();
|
||||
var groupKey = GetGroupingKey(singleViewModel);
|
||||
|
||||
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||
|
||||
// If thread->single conversion is being done, we should ignore it for non-draft items.
|
||||
// eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added.
|
||||
|
||||
if (!PruneSingleNonDraftItems || singleViewModel.IsDraft)
|
||||
{
|
||||
await InsertItemInternalAsync(groupKey, singleViewModel);
|
||||
}
|
||||
}
|
||||
else if (threadMailItemViewModel.ThreadItems.Count == 0)
|
||||
{
|
||||
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Item inside the thread is removed.
|
||||
|
||||
threadMailItemViewModel.ThreadItems.Remove(removalItem);
|
||||
}
|
||||
|
||||
shouldExit = true;
|
||||
break;
|
||||
}
|
||||
else if (item.UniqueId == removeItem.UniqueId)
|
||||
{
|
||||
await RemoveItemInternalAsync(group, item);
|
||||
shouldExit = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action);
|
||||
}
|
||||
}
|
||||
494
Wino.Mail.ViewModels/ComposePageViewModel.cs
Normal file
494
Wino.Mail.ViewModels/ComposePageViewModel.cs
Normal file
@@ -0,0 +1,494 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
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.Navigation;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Messages.Mails;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class ComposePageViewModel : BaseViewModel
|
||||
{
|
||||
public Func<Task<string>> GetHTMLBodyFunction;
|
||||
|
||||
// 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;
|
||||
|
||||
public bool CanSendMail => ComposingAccount != null && !IsLocalDraft && currentMimeMessage != null;
|
||||
|
||||
private MimeMessage currentMimeMessage = null;
|
||||
private readonly BodyBuilder bodyBuilder = new BodyBuilder();
|
||||
|
||||
public bool IsLocalDraft => CurrentMailDraftItem != null
|
||||
&& !string.IsNullOrEmpty(CurrentMailDraftItem.DraftId)
|
||||
&& CurrentMailDraftItem.DraftId.StartsWith(Constants.LocalDraftStartPrefix);
|
||||
|
||||
|
||||
#region Properties
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsLocalDraft))]
|
||||
[NotifyPropertyChangedFor(nameof(CanSendMail))]
|
||||
private MailItemViewModel currentMailDraftItem;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isImportanceSelected;
|
||||
|
||||
[ObservableProperty]
|
||||
private MessageImportance selectedMessageImportance;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCCBCCVisible = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private string subject;
|
||||
|
||||
[ObservableProperty]
|
||||
private MailAccount composingAccount;
|
||||
|
||||
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>();
|
||||
|
||||
public ObservableCollection<MailAccount> Accounts { get; set; } = new ObservableCollection<MailAccount>();
|
||||
public ObservableCollection<AddressInformation> ToItems { get; set; } = new ObservableCollection<AddressInformation>();
|
||||
public ObservableCollection<AddressInformation> CCItemsItems { get; set; } = new ObservableCollection<AddressInformation>();
|
||||
public ObservableCollection<AddressInformation> BCCItems { get; set; } = new ObservableCollection<AddressInformation>();
|
||||
|
||||
|
||||
public List<EditorToolbarSection> ToolbarSections { get; set; } = new List<EditorToolbarSection>()
|
||||
{
|
||||
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format },
|
||||
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert },
|
||||
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Draw },
|
||||
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Options }
|
||||
};
|
||||
|
||||
private EditorToolbarSection selectedToolbarSection;
|
||||
|
||||
public EditorToolbarSection SelectedToolbarSection
|
||||
{
|
||||
get => selectedToolbarSection;
|
||||
set => SetProperty(ref selectedToolbarSection, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public INativeAppService NativeAppService { get; }
|
||||
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILaunchProtocolService _launchProtocolService;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IStatePersistanceService _statePersistanceService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IWinoRequestDelegator _worker;
|
||||
public readonly IContactService ContactService;
|
||||
|
||||
public ComposePageViewModel(IDialogService dialogService,
|
||||
IMailService mailService,
|
||||
ILaunchProtocolService launchProtocolService,
|
||||
IMimeFileService mimeFileService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
INativeAppService nativeAppService,
|
||||
IFolderService folderService,
|
||||
IAccountService accountService,
|
||||
IWinoRequestDelegator worker,
|
||||
IContactService contactService) : base(dialogService)
|
||||
{
|
||||
NativeAppService = nativeAppService;
|
||||
_folderService = folderService;
|
||||
ContactService = contactService;
|
||||
|
||||
_mailService = mailService;
|
||||
_launchProtocolService = launchProtocolService;
|
||||
_mimeFileService = mimeFileService;
|
||||
_statePersistanceService = statePersistanceService;
|
||||
_accountService = accountService;
|
||||
_worker = worker;
|
||||
|
||||
SelectedToolbarSection = ToolbarSections[0];
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveAttachment(MailAttachmentViewModel attachmentViewModel)
|
||||
=> IncludedAttachments.Remove(attachmentViewModel);
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SendAsync()
|
||||
{
|
||||
// TODO: More detailed mail validations.
|
||||
|
||||
if (!ToItems.Any())
|
||||
{
|
||||
await DialogService.ShowMessageAsync(Translator.DialogMessage_ComposerMissingRecipientMessage, Translator.DialogMessage_ComposerValidationFailedTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Subject))
|
||||
{
|
||||
var isConfirmed = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_EmptySubjectConfirmationMessage, Translator.DialogMessage_EmptySubjectConfirmation, Translator.Buttons_Yes);
|
||||
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
|
||||
// Save mime changes before sending.
|
||||
await UpdateMimeChangesAsync().ConfigureAwait(false);
|
||||
|
||||
isUpdatingMimeBlocked = true;
|
||||
|
||||
var assignedAccount = CurrentMailDraftItem.AssignedAccount;
|
||||
|
||||
MailItemFolder sentFolder = null;
|
||||
|
||||
// Load the Sent folder if user wanted to have a copy there.
|
||||
if (assignedAccount.Preferences.ShouldAppendMessagesToSentFolder)
|
||||
{
|
||||
sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||
}
|
||||
|
||||
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, currentMimeMessage, CurrentMailDraftItem.AssignedFolder, sentFolder, CurrentMailDraftItem.AssignedAccount.Preferences);
|
||||
|
||||
await _worker.ExecuteAsync(draftSendPreparationRequest);
|
||||
}
|
||||
|
||||
private async Task UpdateMimeChangesAsync()
|
||||
{
|
||||
if (isUpdatingMimeBlocked || currentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
|
||||
|
||||
// Save recipients.
|
||||
|
||||
SaveAddressInfo(ToItems, currentMimeMessage.To);
|
||||
SaveAddressInfo(CCItemsItems, currentMimeMessage.Cc);
|
||||
SaveAddressInfo(BCCItems, currentMimeMessage.Bcc);
|
||||
|
||||
SaveImportance();
|
||||
SaveSubject();
|
||||
|
||||
await SaveAttachmentsAsync();
|
||||
await SaveBodyAsync();
|
||||
await UpdateMailCopyAsync();
|
||||
|
||||
// Save mime file.
|
||||
await _mimeFileService.SaveMimeMessageAsync(CurrentMailDraftItem.MailCopy.FileId, currentMimeMessage, ComposingAccount.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task UpdateMailCopyAsync()
|
||||
{
|
||||
CurrentMailDraftItem.Subject = currentMimeMessage.Subject;
|
||||
CurrentMailDraftItem.PreviewText = currentMimeMessage.TextBody;
|
||||
|
||||
// Update database.
|
||||
await _mailService.UpdateMailAsync(CurrentMailDraftItem.MailCopy);
|
||||
}
|
||||
|
||||
private async Task SaveAttachmentsAsync()
|
||||
{
|
||||
bodyBuilder.Attachments.Clear();
|
||||
|
||||
foreach (var path in IncludedAttachments)
|
||||
{
|
||||
if (path.Content == null) continue;
|
||||
|
||||
await bodyBuilder.Attachments.AddAsync(path.FileName, new MemoryStream(path.Content));
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveImportance() { currentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal; }
|
||||
|
||||
private void SaveSubject()
|
||||
{
|
||||
if (Subject != null)
|
||||
{
|
||||
currentMimeMessage.Subject = Subject;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveBodyAsync()
|
||||
{
|
||||
if (GetHTMLBodyFunction != null)
|
||||
bodyBuilder.HtmlBody = Regex.Unescape(await GetHTMLBodyFunction());
|
||||
|
||||
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
|
||||
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
|
||||
|
||||
if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null)
|
||||
currentMimeMessage.Body = bodyBuilder.ToMessageBody();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DiscardAsync()
|
||||
{
|
||||
if (ComposingAccount == null)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_MessageCorruptedTitle, Translator.Info_MessageCorruptedMessage, InfoBarMessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmation = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DiscardDraftConfirmationMessage,
|
||||
Translator.DialogMessage_DiscardDraftConfirmationTitle,
|
||||
Translator.Buttons_Yes);
|
||||
|
||||
if (confirmation)
|
||||
{
|
||||
isUpdatingMimeBlocked = true;
|
||||
|
||||
// Don't send delete request for local drafts. Just delete the record and mime locally.
|
||||
if (CurrentMailDraftItem.IsLocalDraft)
|
||||
{
|
||||
await _mailService.DeleteMailAsync(ComposingAccount.Id, CurrentMailDraftItem.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
var deletePackage = new MailOperationPreperationRequest(MailOperation.HardDelete, CurrentMailDraftItem.MailCopy, ignoreHardDeleteProtection: true);
|
||||
await _worker.ExecuteAsync(deletePackage).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
await UpdateMimeChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() => { _statePersistanceService.IsReadingMail = false; });
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters != null && parameters is MailItemViewModel mailItem)
|
||||
{
|
||||
await LoadAccountsAsync();
|
||||
|
||||
CurrentMailDraftItem = mailItem;
|
||||
|
||||
_ = TryPrepareComposeAsync(true);
|
||||
}
|
||||
|
||||
ToItems.CollectionChanged -= ContactListCollectionChanged;
|
||||
ToItems.CollectionChanged += ContactListCollectionChanged;
|
||||
|
||||
_statePersistanceService.IsReadingMail = true;
|
||||
|
||||
// Check if there is any delivering mail address from protocol launch.
|
||||
|
||||
if (_launchProtocolService.MailtoParameters != null)
|
||||
{
|
||||
// TODO
|
||||
//var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems);
|
||||
|
||||
//if (requestedMailContact != null)
|
||||
//{
|
||||
// ToItems.Add(requestedMailContact);
|
||||
//}
|
||||
//else
|
||||
// DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning);
|
||||
|
||||
// Clear the address.
|
||||
_launchProtocolService.MailtoParameters = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ContactListCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
// Prevent duplicates.
|
||||
if (!(sender is ObservableCollection<AddressInformation> list))
|
||||
return;
|
||||
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
if (item is AddressInformation addedInfo && list.Count(a => a == addedInfo) > 1)
|
||||
{
|
||||
var addedIndex = list.IndexOf(addedInfo);
|
||||
list.RemoveAt(addedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAccountsAsync()
|
||||
{
|
||||
// Load accounts
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
Accounts.Add(account);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeComposerAccountAsync()
|
||||
{
|
||||
if (ComposingAccount != null) return true;
|
||||
|
||||
if (CurrentMailDraftItem == null)
|
||||
return false;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ComposingAccount = Accounts.FirstOrDefault(a => a.Id == CurrentMailDraftItem.AssignedAccount.Id);
|
||||
});
|
||||
|
||||
return ComposingAccount != null;
|
||||
}
|
||||
|
||||
private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
|
||||
{
|
||||
if (CurrentMailDraftItem == null)
|
||||
return;
|
||||
|
||||
bool isComposerInitialized = await InitializeComposerAccountAsync();
|
||||
|
||||
if (!isComposerInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Replying existing message.
|
||||
MimeMessageInformation mimeMessageInformation = null;
|
||||
|
||||
try
|
||||
{
|
||||
mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(CurrentMailDraftItem.MailCopy.FileId, ComposingAccount.Id).ConfigureAwait(false);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
if (downloadIfNeeded)
|
||||
{
|
||||
// TODO: Folder id needs to be passed.
|
||||
// TODO: Send mail retrieve request.
|
||||
// _worker.Queue(new FetchSingleItemRequest(ComposingAccount.Id, CurrentMailDraftItem.Id, string.Empty));
|
||||
}
|
||||
//else
|
||||
// DialogService.ShowMIMENotFoundMessage();
|
||||
|
||||
return;
|
||||
}
|
||||
catch (ComposerMimeNotFoundException)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
|
||||
}
|
||||
|
||||
if (mimeMessageInformation == null)
|
||||
return;
|
||||
|
||||
var replyingMime = mimeMessageInformation.MimeMessage;
|
||||
var mimeFilePath = mimeMessageInformation.Path;
|
||||
|
||||
var renderModel = _mimeFileService.GetMailRenderModel(replyingMime, mimeFilePath);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
// Extract information
|
||||
|
||||
ToItems.Clear();
|
||||
CCItemsItems.Clear();
|
||||
BCCItems.Clear();
|
||||
|
||||
LoadAddressInfo(replyingMime.To, ToItems);
|
||||
LoadAddressInfo(replyingMime.Cc, CCItemsItems);
|
||||
LoadAddressInfo(replyingMime.Bcc, BCCItems);
|
||||
|
||||
LoadAttachments(replyingMime.Attachments);
|
||||
|
||||
Subject = replyingMime.Subject;
|
||||
|
||||
currentMimeMessage = replyingMime;
|
||||
|
||||
OnPropertyChanged(nameof(CanSendMail));
|
||||
Messenger.Send(new CreateNewComposeMailRequested(renderModel));
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadAttachments(IEnumerable<MimeEntity> mimeEntities)
|
||||
{
|
||||
foreach (var attachment in mimeEntities)
|
||||
{
|
||||
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
|
||||
{
|
||||
IncludedAttachments.Add(new MailAttachmentViewModel(attachmentPart));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is MailboxAddress mailboxAddress)
|
||||
collection.Add(mailboxAddress.ToAddressInformation());
|
||||
else if (item is GroupAddress groupAddress)
|
||||
LoadAddressInfo(groupAddress.Members, collection);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveAddressInfo(IEnumerable<AddressInformation> addresses, InternetAddressList list)
|
||||
{
|
||||
list.Clear();
|
||||
|
||||
foreach (var item in addresses)
|
||||
list.Add(new MailboxAddress(item.Name, item.Address));
|
||||
}
|
||||
|
||||
public async Task<AddressInformation> GetAddressInformationAsync(string tokenText, ObservableCollection<AddressInformation> collection)
|
||||
{
|
||||
// Get model from the service. This will make sure the name is properly included if there is any record.
|
||||
|
||||
var info = await ContactService.GetAddressInformationByAddressAsync(tokenText);
|
||||
|
||||
// Don't add if there is already that address in the collection.
|
||||
if (collection.Any(a => a.Address == info.Address))
|
||||
return null;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void NotifyAddressExists()
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_ContactExistsTitle, Translator.Info_ContactExistsMessage, InfoBarMessageType.Warning);
|
||||
}
|
||||
|
||||
public void NotifyInvalidEmail(string address)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_InvalidAddressTitle, string.Format(Translator.Info_InvalidAddressMessage, address), InfoBarMessageType.Warning);
|
||||
}
|
||||
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail)
|
||||
{
|
||||
base.OnMailUpdated(updatedMail);
|
||||
|
||||
if (CurrentMailDraftItem == null) return;
|
||||
|
||||
if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
CurrentMailDraftItem.Update(updatedMail);
|
||||
OnPropertyChanged(nameof(CanSendMail));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs
Normal file
26
Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public partial class AccountProviderDetailViewModel : ObservableObject, IAccountProviderDetailViewModel
|
||||
{
|
||||
|
||||
[ObservableProperty]
|
||||
private MailAccount account;
|
||||
|
||||
public IProviderDetail ProviderDetail { get; set; }
|
||||
|
||||
public Guid StartupEntityId => Account.Id;
|
||||
|
||||
public string StartupEntityTitle => Account.Name;
|
||||
|
||||
public AccountProviderDetailViewModel(IProviderDetail providerDetail, MailAccount account)
|
||||
{
|
||||
ProviderDetail = providerDetail;
|
||||
Account = account;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Wino.Mail.ViewModels/Data/AppColorViewModel.cs
Normal file
23
Wino.Mail.ViewModels/Data/AppColorViewModel.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public class AppColorViewModel : ObservableObject
|
||||
{
|
||||
private string _hex;
|
||||
|
||||
public string Hex
|
||||
{
|
||||
get => _hex;
|
||||
set => SetProperty(ref _hex, value);
|
||||
}
|
||||
|
||||
public bool IsAccentColor { get; }
|
||||
|
||||
public AppColorViewModel(string hex, bool isAccentColor = false)
|
||||
{
|
||||
IsAccentColor = isAccentColor;
|
||||
Hex = hex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Messages.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public class BreadcrumbNavigationItemViewModel : ObservableObject
|
||||
{
|
||||
public BreadcrumbNavigationRequested Request { get; set; }
|
||||
|
||||
public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
|
||||
{
|
||||
Request = request;
|
||||
Title = request.PageTitle;
|
||||
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
private string title;
|
||||
public string Title
|
||||
{
|
||||
get => title;
|
||||
set => SetProperty(ref title, value);
|
||||
}
|
||||
|
||||
private bool isActive;
|
||||
|
||||
public bool IsActive
|
||||
{
|
||||
get => isActive;
|
||||
set => SetProperty(ref isActive, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Wino.Mail.ViewModels/Data/FolderPivotViewModel.cs
Normal file
33
Wino.Mail.ViewModels/Data/FolderPivotViewModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
[DebuggerDisplay("{FolderTitle}")]
|
||||
public partial class FolderPivotViewModel : ObservableObject
|
||||
{
|
||||
public bool? IsFocused { get; set; }
|
||||
public string FolderTitle { get; }
|
||||
|
||||
public bool ShouldDisplaySelectedItemCount => IsExtendedMode ? SelectedItemCount > 1 : SelectedItemCount > 0;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSelected;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShouldDisplaySelectedItemCount))]
|
||||
private int selectedItemCount;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShouldDisplaySelectedItemCount))]
|
||||
private bool isExtendedMode = true;
|
||||
|
||||
public FolderPivotViewModel(string folderName, bool? isFocused)
|
||||
{
|
||||
IsFocused = isFocused;
|
||||
|
||||
FolderTitle = IsFocused == null ? folderName : (IsFocused == true ? Translator.Focused : Translator.Other);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs
Normal file
100
Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.IO;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Extensions;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public class MailAttachmentViewModel : ObservableObject
|
||||
{
|
||||
private bool isBusy;
|
||||
private readonly MimePart _mimePart;
|
||||
|
||||
public MailAttachmentType AttachmentType { get; }
|
||||
public string FileName { get; }
|
||||
public string FilePath { get; set; }
|
||||
public string ReadableSize { get; }
|
||||
public byte[] Content { get; set; }
|
||||
|
||||
public IMimeContent MimeContent => _mimePart.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether attachment is busy with opening or saving etc.
|
||||
/// </summary>
|
||||
public bool IsBusy
|
||||
{
|
||||
get => isBusy;
|
||||
set => SetProperty(ref isBusy, value);
|
||||
}
|
||||
|
||||
public MailAttachmentViewModel(MimePart mimePart)
|
||||
{
|
||||
_mimePart = mimePart;
|
||||
|
||||
var array = new byte[_mimePart.Content.Stream.Length];
|
||||
_mimePart.Content.Stream.Read(array, 0, (int)_mimePart.Content.Stream.Length);
|
||||
|
||||
Content = array;
|
||||
|
||||
FileName = mimePart.FileName;
|
||||
ReadableSize = mimePart.Content.Stream.Length.GetBytesReadable();
|
||||
|
||||
var extension = Path.GetExtension(FileName);
|
||||
AttachmentType = GetAttachmentType(extension);
|
||||
}
|
||||
|
||||
public MailAttachmentViewModel(string fullFilePath, byte[] content)
|
||||
{
|
||||
Content = content;
|
||||
|
||||
FileName = Path.GetFileName(fullFilePath);
|
||||
FilePath = fullFilePath;
|
||||
|
||||
ReadableSize = ((long)content.Length).GetBytesReadable();
|
||||
|
||||
var extension = Path.GetExtension(FileName);
|
||||
AttachmentType = GetAttachmentType(extension);
|
||||
}
|
||||
|
||||
public MailAttachmentType GetAttachmentType(string mediaSubtype)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mediaSubtype))
|
||||
return MailAttachmentType.None;
|
||||
|
||||
switch (mediaSubtype.ToLower())
|
||||
{
|
||||
case ".exe":
|
||||
return MailAttachmentType.Executable;
|
||||
case ".rar":
|
||||
return MailAttachmentType.RarArchive;
|
||||
case ".zip":
|
||||
return MailAttachmentType.Archive;
|
||||
case ".ogg":
|
||||
case ".mp3":
|
||||
case ".wav":
|
||||
case ".aac":
|
||||
case ".alac":
|
||||
return MailAttachmentType.Audio;
|
||||
case ".mp4":
|
||||
case ".wmv":
|
||||
case ".avi":
|
||||
case ".flv":
|
||||
return MailAttachmentType.Video;
|
||||
case ".pdf":
|
||||
return MailAttachmentType.PDF;
|
||||
case ".htm":
|
||||
case ".html":
|
||||
return MailAttachmentType.HTML;
|
||||
case ".png":
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
case ".gif":
|
||||
case ".jiff":
|
||||
return MailAttachmentType.Image;
|
||||
default:
|
||||
return MailAttachmentType.Other;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Wino.Mail.ViewModels/Data/MailItemContainer.cs
Normal file
20
Wino.Mail.ViewModels/Data/MailItemContainer.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public class MailItemContainer
|
||||
{
|
||||
public MailItemViewModel ItemViewModel { get; set; }
|
||||
public ThreadMailItemViewModel ThreadViewModel { get; set; }
|
||||
|
||||
public MailItemContainer(MailItemViewModel itemViewModel, ThreadMailItemViewModel threadViewModel) : this(itemViewModel)
|
||||
{
|
||||
ThreadViewModel = threadViewModel ?? throw new ArgumentNullException(nameof(threadViewModel));
|
||||
}
|
||||
|
||||
public MailItemContainer(MailItemViewModel itemViewModel)
|
||||
{
|
||||
ItemViewModel = itemViewModel ?? throw new ArgumentNullException(nameof(itemViewModel));
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Wino.Mail.ViewModels/Data/MailItemViewModel.cs
Normal file
106
Wino.Mail.ViewModels/Data/MailItemViewModel.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Single view model for IMailItem representation.
|
||||
/// </summary>
|
||||
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem
|
||||
{
|
||||
public MailCopy MailCopy { get; private set; } = mailCopy;
|
||||
|
||||
public bool IsLocalDraft => !string.IsNullOrEmpty(DraftId) && DraftId.StartsWith(Constants.LocalDraftStartPrefix);
|
||||
|
||||
public Guid UniqueId => ((IMailItem)MailCopy).UniqueId;
|
||||
public string ThreadId => ((IMailItem)MailCopy).ThreadId;
|
||||
public string MessageId => ((IMailItem)MailCopy).MessageId;
|
||||
public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress;
|
||||
public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate;
|
||||
public string FromAddress => ((IMailItem)MailCopy).FromAddress;
|
||||
public bool HasAttachments => ((IMailItem)MailCopy).HasAttachments;
|
||||
public string References => ((IMailItem)MailCopy).References;
|
||||
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCustomFocused;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSelected;
|
||||
|
||||
public bool IsFlagged
|
||||
{
|
||||
get => MailCopy.IsFlagged;
|
||||
set => SetProperty(MailCopy.IsFlagged, value, MailCopy, (u, n) => u.IsFlagged = n);
|
||||
}
|
||||
|
||||
public bool IsFocused
|
||||
{
|
||||
get => MailCopy.IsFocused;
|
||||
set => SetProperty(MailCopy.IsFocused, value, MailCopy, (u, n) => u.IsFocused = n);
|
||||
}
|
||||
|
||||
public bool IsRead
|
||||
{
|
||||
get => MailCopy.IsRead;
|
||||
set => SetProperty(MailCopy.IsRead, value, MailCopy, (u, n) => u.IsRead = n);
|
||||
}
|
||||
|
||||
public bool IsDraft
|
||||
{
|
||||
get => MailCopy.IsDraft;
|
||||
set => SetProperty(MailCopy.IsDraft, value, MailCopy, (u, n) => u.IsDraft = n);
|
||||
}
|
||||
|
||||
public string DraftId
|
||||
{
|
||||
get => MailCopy.DraftId;
|
||||
set => SetProperty(MailCopy.DraftId, value, MailCopy, (u, n) => u.DraftId = n);
|
||||
}
|
||||
|
||||
public string Id
|
||||
{
|
||||
get => MailCopy.Id;
|
||||
set => SetProperty(MailCopy.Id, value, MailCopy, (u, n) => u.Id = n);
|
||||
}
|
||||
|
||||
public string Subject
|
||||
{
|
||||
get => MailCopy.Subject;
|
||||
set => SetProperty(MailCopy.Subject, value, MailCopy, (u, n) => u.Subject = n);
|
||||
}
|
||||
|
||||
public string PreviewText
|
||||
{
|
||||
get => MailCopy.PreviewText;
|
||||
set => SetProperty(MailCopy.PreviewText, value, MailCopy, (u, n) => u.PreviewText = n);
|
||||
}
|
||||
|
||||
public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder;
|
||||
|
||||
public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount;
|
||||
|
||||
public Guid FileId => ((IMailItem)MailCopy).FileId;
|
||||
|
||||
public void Update(MailCopy updatedMailItem)
|
||||
{
|
||||
MailCopy = updatedMailItem;
|
||||
|
||||
// DEBUG
|
||||
//if (updatedMailItem.AssignedAccount == null || updatedMailItem.AssignedFolder == null)
|
||||
// throw new Exception("Assigned account or folder is null.");
|
||||
|
||||
OnPropertyChanged(nameof(IsRead));
|
||||
OnPropertyChanged(nameof(IsFocused));
|
||||
OnPropertyChanged(nameof(IsFlagged));
|
||||
OnPropertyChanged(nameof(IsDraft));
|
||||
OnPropertyChanged(nameof(DraftId));
|
||||
OnPropertyChanged(nameof(Subject));
|
||||
OnPropertyChanged(nameof(PreviewText));
|
||||
OnPropertyChanged(nameof(IsLocalDraft));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public partial class MergedAccountProviderDetailViewModel : ObservableObject, IAccountProviderDetailViewModel
|
||||
{
|
||||
public List<AccountProviderDetailViewModel> HoldingAccounts { get; }
|
||||
public MergedInbox MergedInbox { get; }
|
||||
|
||||
public string AccountAddresses => string.Join(", ", HoldingAccounts.Select(a => a.Account.Address));
|
||||
|
||||
public Guid StartupEntityId => MergedInbox.Id;
|
||||
|
||||
public string StartupEntityTitle => MergedInbox.Name;
|
||||
|
||||
public MergedAccountProviderDetailViewModel(MergedInbox mergedInbox, List<AccountProviderDetailViewModel> holdingAccounts)
|
||||
{
|
||||
MergedInbox = mergedInbox;
|
||||
HoldingAccounts = holdingAccounts;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs
Normal file
127
Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread mail item (multiple IMailItem) view model representation.
|
||||
/// </summary>
|
||||
public class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable<string>, IComparable<DateTime>
|
||||
{
|
||||
public ObservableCollection<IMailItem> ThreadItems => ((IMailItemThread)_threadMailItem).ThreadItems;
|
||||
|
||||
private readonly ThreadMailItem _threadMailItem;
|
||||
|
||||
private bool isThreadExpanded;
|
||||
public bool IsThreadExpanded
|
||||
{
|
||||
get => isThreadExpanded;
|
||||
set => SetProperty(ref isThreadExpanded, value);
|
||||
}
|
||||
|
||||
public ThreadMailItemViewModel(ThreadMailItem threadMailItem)
|
||||
{
|
||||
_threadMailItem = new ThreadMailItem();
|
||||
|
||||
// Local copies
|
||||
foreach (var item in threadMailItem.ThreadItems)
|
||||
{
|
||||
AddMailItemViewModel(item);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<MailCopy> GetMailCopies()
|
||||
=> ThreadItems.OfType<MailItemViewModel>().Select(a => a.MailCopy);
|
||||
|
||||
public void AddMailItemViewModel(IMailItem mailItem)
|
||||
{
|
||||
if (mailItem == null) return;
|
||||
|
||||
if (mailItem is MailCopy mailCopy)
|
||||
_threadMailItem.AddThreadItem(new MailItemViewModel(mailCopy));
|
||||
else if (mailItem is MailItemViewModel mailItemViewModel)
|
||||
_threadMailItem.AddThreadItem(mailItemViewModel);
|
||||
else
|
||||
Debugger.Break();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
OnPropertyChanged(nameof(Subject));
|
||||
OnPropertyChanged(nameof(PreviewText));
|
||||
OnPropertyChanged(nameof(FromName));
|
||||
OnPropertyChanged(nameof(FromAddress));
|
||||
OnPropertyChanged(nameof(HasAttachments));
|
||||
|
||||
OnPropertyChanged(nameof(IsFlagged));
|
||||
OnPropertyChanged(nameof(IsDraft));
|
||||
OnPropertyChanged(nameof(IsRead));
|
||||
OnPropertyChanged(nameof(IsFocused));
|
||||
OnPropertyChanged(nameof(CreationDate));
|
||||
}
|
||||
|
||||
public IMailItem LatestMailItem => ((IMailItemThread)_threadMailItem).LatestMailItem;
|
||||
public IMailItem FirstMailItem => ((IMailItemThread)_threadMailItem).FirstMailItem;
|
||||
|
||||
public string Id => ((IMailItem)_threadMailItem).Id;
|
||||
public string Subject => ((IMailItem)_threadMailItem).Subject;
|
||||
public string ThreadId => ((IMailItem)_threadMailItem).ThreadId;
|
||||
public string MessageId => ((IMailItem)_threadMailItem).MessageId;
|
||||
public string References => ((IMailItem)_threadMailItem).References;
|
||||
public string PreviewText => ((IMailItem)_threadMailItem).PreviewText;
|
||||
public string FromName => ((IMailItem)_threadMailItem).FromName;
|
||||
public DateTime CreationDate => ((IMailItem)_threadMailItem).CreationDate;
|
||||
public string FromAddress => ((IMailItem)_threadMailItem).FromAddress;
|
||||
public bool HasAttachments => ((IMailItem)_threadMailItem).HasAttachments;
|
||||
public bool IsFlagged => ((IMailItem)_threadMailItem).IsFlagged;
|
||||
public bool IsFocused => ((IMailItem)_threadMailItem).IsFocused;
|
||||
public bool IsRead => ((IMailItem)_threadMailItem).IsRead;
|
||||
public bool IsDraft => ((IMailItem)_threadMailItem).IsDraft;
|
||||
public string DraftId => string.Empty;
|
||||
public string InReplyTo => ((IMailItem)_threadMailItem).InReplyTo;
|
||||
|
||||
public MailItemFolder AssignedFolder => ((IMailItem)_threadMailItem).AssignedFolder;
|
||||
|
||||
public MailAccount AssignedAccount => ((IMailItem)_threadMailItem).AssignedAccount;
|
||||
|
||||
public Guid UniqueId => ((IMailItem)_threadMailItem).UniqueId;
|
||||
|
||||
public Guid FileId => ((IMailItem)_threadMailItem).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;
|
||||
}
|
||||
}
|
||||
8
Wino.Mail.ViewModels/GlobalSuppressions.cs
Normal file
8
Wino.Mail.ViewModels/GlobalSuppressions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~F:Wino.Mail.ViewModels.ComposePageViewModel.isImportanceSelected")]
|
||||
38
Wino.Mail.ViewModels/IdlePageViewModel.cs
Normal file
38
Wino.Mail.ViewModels/IdlePageViewModel.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Messages.Mails;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class IdlePageViewModel : BaseViewModel, IRecipient<SelectedMailItemsChanged>
|
||||
{
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasSelectedItems))]
|
||||
[NotifyPropertyChangedFor(nameof(SelectedMessageText))]
|
||||
private int selectedItemCount;
|
||||
|
||||
public bool HasSelectedItems => SelectedItemCount > 0;
|
||||
|
||||
public string SelectedMessageText => HasSelectedItems ? string.Format(Translator.MailsSelected, SelectedItemCount) : Translator.NoMailSelected;
|
||||
|
||||
public IdlePageViewModel(IDialogService dialogService) : base(dialogService) { }
|
||||
|
||||
public void Receive(SelectedMailItemsChanged message)
|
||||
{
|
||||
SelectedItemCount = message.SelectedItemCount;
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters != null && parameters is int selectedItemCount)
|
||||
SelectedItemCount = selectedItemCount;
|
||||
else
|
||||
SelectedItemCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
922
Wino.Mail.ViewModels/MailListPageViewModel.cs
Normal file
922
Wino.Mail.ViewModels/MailListPageViewModel.cs
Normal file
@@ -0,0 +1,922 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.AppCenter.Crashes;
|
||||
using MoreLinq;
|
||||
using Nito.AsyncEx;
|
||||
using Serilog;
|
||||
using Wino.Core;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Messages.Mails;
|
||||
using Wino.Core.Messages.Synchronization;
|
||||
using Wino.Mail.ViewModels.Collections;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class MailListPageViewModel : BaseViewModel,
|
||||
IRecipient<MailItemNavigationRequested>,
|
||||
IRecipient<ActiveMailFolderChangedEvent>,
|
||||
IRecipient<MailItemSelectedEvent>,
|
||||
IRecipient<MailItemSelectionRemovedEvent>,
|
||||
IRecipient<AccountSynchronizationCompleted>,
|
||||
IRecipient<NewSynchronizationRequested>,
|
||||
IRecipient<AccountSynchronizerStateChanged>
|
||||
{
|
||||
private bool isChangingFolder = false;
|
||||
|
||||
private Guid? trackingSynchronizationId = null;
|
||||
private int completedTrackingSynchronizationCount = 0;
|
||||
|
||||
private IObservable<System.Reactive.EventPattern<NotifyCollectionChangedEventArgs>> selectionChangedObservable = null;
|
||||
|
||||
public WinoMailCollection MailCollection { get; } = new WinoMailCollection();
|
||||
|
||||
public ObservableCollection<MailItemViewModel> SelectedItems { get; set; } = new ObservableCollection<MailItemViewModel>();
|
||||
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = new ObservableCollection<FolderPivotViewModel>();
|
||||
|
||||
private readonly SemaphoreSlim listManipulationSemepahore = new SemaphoreSlim(1);
|
||||
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public IWinoNavigationService NavigationService { get; }
|
||||
public IStatePersistanceService StatePersistanceService { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
private readonly IMailService _mailService;
|
||||
private readonly INotificationBuilder _notificationBuilder;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IWinoSynchronizerFactory _winoSynchronizerFactory;
|
||||
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
|
||||
private readonly IContextMenuItemService _contextMenuItemService;
|
||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||
private readonly IKeyPressService _keyPressService;
|
||||
|
||||
private MailItemViewModel _activeMailItem;
|
||||
|
||||
public List<SortingOption> SortingOptions { get; } =
|
||||
[
|
||||
new(Translator.SortingOption_Date, SortingOptionType.ReceiveDate),
|
||||
new(Translator.SortingOption_Name, SortingOptionType.Sender),
|
||||
];
|
||||
|
||||
public List<FilterOption> FilterOptions { get; } =
|
||||
[
|
||||
new (Translator.FilteringOption_All, FilterOptionType.All),
|
||||
new (Translator.FilteringOption_Unread, FilterOptionType.Unread),
|
||||
new (Translator.FilteringOption_Flagged, FilterOptionType.Flagged)
|
||||
];
|
||||
|
||||
private FolderPivotViewModel _selectedFolderPivot;
|
||||
|
||||
[ObservableProperty]
|
||||
private string searchQuery;
|
||||
|
||||
[ObservableProperty]
|
||||
private FilterOption _selectedFilterOption;
|
||||
private SortingOption _selectedSortingOption;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsEmpty))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCriteriaFailed))]
|
||||
[NotifyPropertyChangedFor(nameof(IsFolderEmpty))]
|
||||
private bool isInitializingFolder;
|
||||
|
||||
[ObservableProperty]
|
||||
private InfoBarMessageType barSeverity;
|
||||
|
||||
[ObservableProperty]
|
||||
private string barMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private string barTitle;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isBarOpen;
|
||||
|
||||
public MailListPageViewModel(IDialogService dialogService,
|
||||
IWinoNavigationService navigationService,
|
||||
IMailService mailService,
|
||||
INotificationBuilder notificationBuilder,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IFolderService folderService,
|
||||
IWinoSynchronizerFactory winoSynchronizerFactory,
|
||||
IThreadingStrategyProvider threadingStrategyProvider,
|
||||
IContextMenuItemService contextMenuItemService,
|
||||
IWinoRequestDelegator winoRequestDelegator,
|
||||
IKeyPressService keyPressService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
StatePersistanceService = statePersistanceService;
|
||||
NavigationService = navigationService;
|
||||
|
||||
_mailService = mailService;
|
||||
_notificationBuilder = notificationBuilder;
|
||||
_folderService = folderService;
|
||||
_winoSynchronizerFactory = winoSynchronizerFactory;
|
||||
_threadingStrategyProvider = threadingStrategyProvider;
|
||||
_contextMenuItemService = contextMenuItemService;
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_keyPressService = keyPressService;
|
||||
|
||||
SelectedFilterOption = FilterOptions[0];
|
||||
SelectedSortingOption = SortingOptions[0];
|
||||
|
||||
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged));
|
||||
selectionChangedObservable
|
||||
.Throttle(TimeSpan.FromMilliseconds(100))
|
||||
.Subscribe(async a =>
|
||||
{
|
||||
await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); });
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the requested mail operation for currently selected items.
|
||||
/// </summary>
|
||||
/// <param name="operation">Action to execute for selected items.</param>
|
||||
[RelayCommand]
|
||||
private async Task MailOperationAsync(int mailOperationIndex)
|
||||
{
|
||||
if (!SelectedItems.Any()) return;
|
||||
|
||||
// Commands don't like enums. So it has to be int.
|
||||
var operation = (MailOperation)mailOperationIndex;
|
||||
|
||||
var package = new MailOperationPreperationRequest(operation, SelectedItems.Select(a => a.MailCopy));
|
||||
|
||||
await ExecuteMailOperationAsync(package);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sens a new message to synchronize current folder.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void SyncFolder()
|
||||
{
|
||||
if (!CanSynchronize) return;
|
||||
|
||||
// Only synchronize listed folders.
|
||||
|
||||
// When doing linked inbox sync, we need to save the sync id to report progress back only once.
|
||||
// Otherwise, we will report progress for each folder and that's what we don't want.
|
||||
|
||||
trackingSynchronizationId = Guid.NewGuid();
|
||||
completedTrackingSynchronizationCount = 0;
|
||||
|
||||
foreach (var folder in ActiveFolder.HandlingFolders)
|
||||
{
|
||||
var options = new SynchronizationOptions()
|
||||
{
|
||||
AccountId = folder.MailAccountId,
|
||||
Type = SynchronizationType.Custom,
|
||||
SynchronizationFolderIds = [folder.Id],
|
||||
GroupedSynchronizationTrackingId = trackingSynchronizationId
|
||||
};
|
||||
|
||||
Messenger.Send(new NewSynchronizationRequested(options));
|
||||
}
|
||||
}
|
||||
|
||||
private async void ActiveMailItemChanged(MailItemViewModel selectedMailItemViewModel)
|
||||
{
|
||||
if (_activeMailItem == selectedMailItemViewModel) return;
|
||||
|
||||
// Don't update active mail item if Ctrl key is pressed.
|
||||
// User is probably trying to select multiple items.
|
||||
// This is not the same behavior in Windows Mail,
|
||||
// but it's a trash behavior.
|
||||
|
||||
var isCtrlKeyPressed = _keyPressService.IsCtrlKeyPressed();
|
||||
|
||||
if (isCtrlKeyPressed) return;
|
||||
|
||||
_activeMailItem = selectedMailItemViewModel;
|
||||
|
||||
Messenger.Send(new ActiveMailItemChangedEvent(selectedMailItemViewModel));
|
||||
|
||||
if (selectedMailItemViewModel == null) return;
|
||||
|
||||
// Automatically set mark as read or not based on preferences.
|
||||
|
||||
var markAsPreference = PreferencesService.MarkAsPreference;
|
||||
|
||||
if (markAsPreference == MailMarkAsOption.WhenSelected)
|
||||
{
|
||||
if (selectedMailItemViewModel != null && !selectedMailItemViewModel.IsRead)
|
||||
{
|
||||
var operation = MailOperation.MarkAsRead;
|
||||
var package = new MailOperationPreperationRequest(operation,_activeMailItem.MailCopy);
|
||||
|
||||
await ExecuteMailOperationAsync(package);
|
||||
}
|
||||
}
|
||||
else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0)
|
||||
{
|
||||
// TODO: Start a timer then queue.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selected internal folder. This can be either folder's own name or Focused-Other.
|
||||
/// </summary>
|
||||
public FolderPivotViewModel SelectedFolderPivot
|
||||
{
|
||||
get => _selectedFolderPivot;
|
||||
set
|
||||
{
|
||||
if (_selectedFolderPivot != null)
|
||||
_selectedFolderPivot.SelectedItemCount = 0;
|
||||
|
||||
SetProperty(ref _selectedFolderPivot, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selected sorting option.
|
||||
/// </summary>
|
||||
public SortingOption SelectedSortingOption
|
||||
{
|
||||
get => _selectedSortingOption;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedSortingOption, value))
|
||||
{
|
||||
if (value != null && MailCollection != null)
|
||||
{
|
||||
MailCollection.SortingType = value.Type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current folder that is being represented from the menu.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanSynchronize))]
|
||||
[NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))]
|
||||
private IBaseFolderMenuItem activeFolder;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanSynchronize))]
|
||||
private bool isAccountSynchronizerInSynchronization;
|
||||
|
||||
public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
|
||||
|
||||
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
||||
|
||||
#region Properties
|
||||
|
||||
public int SelectedItemCount => SelectedItems.Count;
|
||||
public bool HasMultipleItemSelections => SelectedItemCount > 1;
|
||||
public bool HasSelectedItems => SelectedItems.Any();
|
||||
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
||||
public bool IsEmpty => !IsPerformingSearch && MailCollection.Count == 0;
|
||||
public bool IsCriteriaFailed => IsEmpty && IsInSearchMode;
|
||||
public bool IsFolderEmpty => !IsInitializingFolder && IsEmpty && !IsInSearchMode;
|
||||
|
||||
private bool _isPerformingSearch;
|
||||
|
||||
public bool IsPerformingSearch
|
||||
{
|
||||
get => _isPerformingSearch;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isPerformingSearch, value))
|
||||
{
|
||||
NotifyItemFoundState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInSearchMode => !string.IsNullOrEmpty(SearchQuery);
|
||||
|
||||
#endregion
|
||||
|
||||
public void NotifyItemSelected()
|
||||
{
|
||||
OnPropertyChanged(nameof(HasSelectedItems));
|
||||
OnPropertyChanged(nameof(SelectedItemCount));
|
||||
OnPropertyChanged(nameof(HasMultipleItemSelections));
|
||||
|
||||
if (SelectedFolderPivot != null)
|
||||
SelectedFolderPivot.SelectedItemCount = SelectedItemCount;
|
||||
}
|
||||
|
||||
private void NotifyItemFoundState()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
OnPropertyChanged(nameof(IsCriteriaFailed));
|
||||
OnPropertyChanged(nameof(IsFolderEmpty));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request);
|
||||
|
||||
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package);
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
|
||||
MailCollection.CoreDispatcher = Dispatcher;
|
||||
}
|
||||
|
||||
protected override async void OnFolderUpdated(MailItemFolder updatedFolder, MailAccount account)
|
||||
{
|
||||
base.OnFolderUpdated(updatedFolder, account);
|
||||
|
||||
// Don't need to update if the folder update does not belong to the current folder menu item.
|
||||
if (ActiveFolder == null || updatedFolder == null || !ActiveFolder.HandlingFolders.Any(a => a.Id == updatedFolder.Id)) return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ActiveFolder.UpdateFolder(updatedFolder);
|
||||
|
||||
OnPropertyChanged(nameof(CanSynchronize));
|
||||
OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
|
||||
});
|
||||
|
||||
// Force synchronization after enabling the folder.
|
||||
SyncFolder();
|
||||
}
|
||||
|
||||
private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
BarSeverity = severity;
|
||||
BarTitle = title;
|
||||
BarMessage = message;
|
||||
|
||||
IsBarOpen = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void SelectedItemCollectionUpdated(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (SelectedItems.Count == 1)
|
||||
{
|
||||
ActiveMailItemChanged(SelectedItems[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// At this point, either we don't have any item selected
|
||||
// or we have multiple item selected. In either case
|
||||
// there should be no active item.
|
||||
|
||||
ActiveMailItemChanged(null);
|
||||
}
|
||||
|
||||
NotifyItemSelected();
|
||||
|
||||
Messenger.Send(new SelectedMailItemsChanged(SelectedItems.Count));
|
||||
}
|
||||
|
||||
private void UpdateFolderPivots()
|
||||
{
|
||||
PivotFolders.Clear();
|
||||
SelectedFolderPivot = null;
|
||||
|
||||
if (ActiveFolder == null) return;
|
||||
|
||||
// Merged folders don't support focused feature.
|
||||
|
||||
if (ActiveFolder is IMergedAccountFolderMenuItem)
|
||||
{
|
||||
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
|
||||
}
|
||||
else if (ActiveFolder is IFolderMenuItem singleFolderMenuItem)
|
||||
{
|
||||
var parentAccount = singleFolderMenuItem.ParentAccount;
|
||||
|
||||
bool isAccountSupportsFocusedInbox = parentAccount.Preferences.IsFocusedInboxEnabled != null;
|
||||
bool isFocusedInboxEnabled = isAccountSupportsFocusedInbox && parentAccount.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
|
||||
bool isInboxFolder = ActiveFolder.SpecialFolderType == SpecialFolderType.Inbox;
|
||||
|
||||
// Folder supports Focused - Other
|
||||
if (isInboxFolder && isFocusedInboxEnabled)
|
||||
{
|
||||
// Can be passed as empty string. Focused - Other will be used regardless.
|
||||
var focusedItem = new FolderPivotViewModel(string.Empty, true);
|
||||
var otherItem = new FolderPivotViewModel(string.Empty, false);
|
||||
|
||||
PivotFolders.Add(focusedItem);
|
||||
PivotFolders.Add(otherItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the account and folder doesn't support focused feature, just add itself.
|
||||
PivotFolders.Add(new FolderPivotViewModel(singleFolderMenuItem.FolderName, null));
|
||||
}
|
||||
}
|
||||
|
||||
// This will trigger refresh.
|
||||
SelectedFolderPivot = PivotFolders.FirstOrDefault();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectedPivotChanged()
|
||||
{
|
||||
if (isChangingFolder) return;
|
||||
|
||||
await InitializeFolderAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectedSortingChanged(SortingOption option)
|
||||
{
|
||||
SelectedSortingOption = option;
|
||||
|
||||
if (isChangingFolder) return;
|
||||
|
||||
await InitializeFolderAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectedFilterChanged(FilterOption option)
|
||||
{
|
||||
SelectedFilterOption = option;
|
||||
|
||||
if (isChangingFolder) return;
|
||||
|
||||
await InitializeFolderAsync();
|
||||
}
|
||||
|
||||
public IEnumerable<MailItemViewModel> GetTargetMailItemViewModels(IMailItem clickedItem)
|
||||
{
|
||||
// Threat threads as a whole and include everything in the group. Except single selections outside of the thread.
|
||||
IEnumerable<MailItemViewModel> contextMailItems = null;
|
||||
|
||||
if (clickedItem is ThreadMailItemViewModel clickedThreadItem)
|
||||
{
|
||||
// Clicked item is a thread.
|
||||
|
||||
clickedThreadItem.IsThreadExpanded = true;
|
||||
contextMailItems = clickedThreadItem.ThreadItems.Cast<MailItemViewModel>();
|
||||
|
||||
// contextMailItems = clickedThreadItem.GetMailCopies();
|
||||
}
|
||||
else if (clickedItem is MailItemViewModel clickedMailItemViewModel)
|
||||
{
|
||||
// If the clicked item is included in SelectedItems, then we need to thing them as whole.
|
||||
// If there are selected items, but clicked item is not one of them, then it's a single context menu.
|
||||
|
||||
bool includedInSelectedItems = SelectedItems.Contains(clickedItem);
|
||||
|
||||
if (includedInSelectedItems)
|
||||
contextMailItems = SelectedItems;
|
||||
else
|
||||
contextMailItems = new List<MailItemViewModel>() { clickedMailItemViewModel };
|
||||
}
|
||||
|
||||
return contextMailItems;
|
||||
}
|
||||
|
||||
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<IMailItem> contextMailItems)
|
||||
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems);
|
||||
|
||||
public void ChangeCustomFocusedState(IEnumerable<IMailItem> mailItems, bool isFocused)
|
||||
=> mailItems.Where(a => a is MailItemViewModel).Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = isFocused);
|
||||
|
||||
private bool ShouldPreventItemAdd(IMailItem mailItem)
|
||||
{
|
||||
bool condition2 = false;
|
||||
|
||||
bool condition1 = mailItem.IsRead
|
||||
&& SelectedFilterOption.Type == FilterOptionType.Unread
|
||||
|| !mailItem.IsFlagged
|
||||
&& SelectedFilterOption.Type == FilterOptionType.Flagged;
|
||||
|
||||
return condition1 || condition2;
|
||||
}
|
||||
|
||||
protected override async void OnMailAdded(MailCopy addedMail)
|
||||
{
|
||||
base.OnMailAdded(addedMail);
|
||||
|
||||
try
|
||||
{
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
if (ActiveFolder == null) return;
|
||||
|
||||
// Messages coming to sent or draft folder must be inserted regardless of the filter.
|
||||
bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
|
||||
addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
|
||||
|
||||
// Item does not belong to this folder and doesn't have special type to be inserted.
|
||||
if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return;
|
||||
|
||||
if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return;
|
||||
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
await MailCollection.AddAsync(addedMail);
|
||||
|
||||
NotifyItemFoundState();
|
||||
});
|
||||
}
|
||||
catch (Exception) { }
|
||||
finally
|
||||
{
|
||||
listManipulationSemepahore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail)
|
||||
{
|
||||
base.OnMailUpdated(updatedMail);
|
||||
|
||||
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
|
||||
|
||||
await MailCollection.UpdateMailCopy(updatedMail);
|
||||
}
|
||||
|
||||
protected override async void OnMailRemoved(MailCopy removedMail)
|
||||
{
|
||||
base.OnMailRemoved(removedMail);
|
||||
|
||||
// We should delete the items only if:
|
||||
// 1. They are deleted from the active folder.
|
||||
// 2. Deleted from draft or sent folder.
|
||||
// Delete/sent are special folders that can list their items in other folders.
|
||||
|
||||
bool removedFromActiveFolder = ActiveFolder.HandlingFolders.Any(a => a.Id == removedMail.AssignedFolder.Id);
|
||||
bool removedFromDraftOrSent = removedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
|
||||
removedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
|
||||
|
||||
if (removedFromActiveFolder || removedFromDraftOrSent)
|
||||
{
|
||||
bool isDeletedMailSelected = SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
|
||||
|
||||
// Automatically select the next item in the list if the setting is enabled.
|
||||
MailItemViewModel nextItem = null;
|
||||
|
||||
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
|
||||
{
|
||||
nextItem = MailCollection.GetNextItem(removedMail);
|
||||
}
|
||||
|
||||
// Remove the deleted item from the list.
|
||||
await MailCollection.RemoveAsync(removedMail);
|
||||
|
||||
if (nextItem != null)
|
||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true));
|
||||
else if (isDeletedMailSelected)
|
||||
{
|
||||
// There are no next item to select, but we removed the last item which was selected.
|
||||
// Clearing selected item will dispose rendering page.
|
||||
|
||||
SelectedItems.Clear();
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
||||
}
|
||||
}
|
||||
protected override async void OnDraftCreated(MailCopy draftMail, MailAccount account)
|
||||
{
|
||||
base.OnDraftCreated(draftMail, account);
|
||||
|
||||
try
|
||||
{
|
||||
// If the draft is created in another folder, we need to wait for that folder to be initialized.
|
||||
// Otherwise the draft mail item will be duplicated on the next add execution.
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
// Create the item. Draft folder navigation is already done at this point.
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
await MailCollection.AddAsync(draftMail);
|
||||
|
||||
// New draft is created by user. Select the item.
|
||||
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
|
||||
|
||||
NotifyItemFoundState();
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
listManipulationSemepahore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IMailItem> PrepareMailViewModels(IEnumerable<IMailItem> mailItems)
|
||||
{
|
||||
foreach (var item in mailItems)
|
||||
{
|
||||
if (item is MailCopy singleMailItem)
|
||||
yield return new MailItemViewModel(singleMailItem);
|
||||
else if (item is ThreadMailItem threadMailItem)
|
||||
yield return new ThreadMailItemViewModel(threadMailItem);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadMoreItemsAsync()
|
||||
{
|
||||
if (IsInitializingFolder) return;
|
||||
|
||||
await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
||||
|
||||
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
||||
SelectedFilterOption.Type,
|
||||
SelectedSortingOption.Type,
|
||||
PreferencesService.IsThreadingEnabled,
|
||||
SelectedFolderPivot.IsFocused,
|
||||
SearchQuery,
|
||||
MailCollection.MailCopyIdHashSet);
|
||||
|
||||
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
|
||||
|
||||
var viewModels = PrepareMailViewModels(items);
|
||||
|
||||
await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); });
|
||||
await ExecuteUIThread(() => { IsInitializingFolder = false; });
|
||||
}
|
||||
|
||||
private async Task InitializeFolderAsync()
|
||||
{
|
||||
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Clear search query if not performing search.
|
||||
if (!IsPerformingSearch)
|
||||
SearchQuery = string.Empty;
|
||||
|
||||
MailCollection.Clear();
|
||||
MailCollection.MailCopyIdHashSet.Clear();
|
||||
|
||||
SelectedItems.Clear();
|
||||
|
||||
if (ActiveFolder == null)
|
||||
return;
|
||||
|
||||
await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
||||
|
||||
// Folder is changed during initialization.
|
||||
// Just cancel the existing one and wait for new initialization.
|
||||
|
||||
if (listManipulationSemepahore.CurrentCount == 0)
|
||||
{
|
||||
Debug.WriteLine("Canceling initialization of mails.");
|
||||
|
||||
listManipulationCancellationTokenSource.Cancel();
|
||||
listManipulationCancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
listManipulationCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
var cancellationToken = listManipulationCancellationTokenSource.Token;
|
||||
|
||||
await listManipulationSemepahore.WaitAsync(cancellationToken);
|
||||
|
||||
// Setup MailCollection configuration.
|
||||
|
||||
// Don't pass any threading strategy if disabled in settings.
|
||||
MailCollection.ThreadingStrategyProvider = PreferencesService.IsThreadingEnabled ? _threadingStrategyProvider : null;
|
||||
|
||||
// TODO: This should go inside
|
||||
MailCollection.PruneSingleNonDraftItems = ActiveFolder.SpecialFolderType == SpecialFolderType.Draft;
|
||||
|
||||
// Here items are sorted and filtered.
|
||||
|
||||
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
||||
SelectedFilterOption.Type,
|
||||
SelectedSortingOption.Type,
|
||||
PreferencesService.IsThreadingEnabled,
|
||||
SelectedFolderPivot.IsFocused,
|
||||
SearchQuery,
|
||||
MailCollection.MailCopyIdHashSet);
|
||||
|
||||
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
|
||||
|
||||
// Here they are already threaded if needed.
|
||||
// We don't need to insert them one by one.
|
||||
// Just create VMs and do bulk insert.
|
||||
|
||||
var viewModels = PrepareMailViewModels(items);
|
||||
|
||||
await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, true); });
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("Initialization of mails canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debugger.Break();
|
||||
|
||||
if (IsInSearchMode)
|
||||
Log.Error(ex, WinoErrors.SearchFailed);
|
||||
else
|
||||
Log.Error(ex, WinoErrors.MailListRefreshFolder);
|
||||
|
||||
Crashes.TrackError(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
listManipulationSemepahore.Release();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsInitializingFolder = false;
|
||||
|
||||
OnPropertyChanged(nameof(CanSynchronize));
|
||||
NotifyItemFoundState();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EnableFolderSynchronizationAsync()
|
||||
{
|
||||
if (ActiveFolder == null) return;
|
||||
|
||||
foreach (var folder in ActiveFolder.HandlingFolders)
|
||||
{
|
||||
await _folderService.ChangeFolderSynchronizationStateAsync(folder.Id, true);
|
||||
}
|
||||
|
||||
// TODO
|
||||
//ActiveFolder.IsSynchronizationEnabled = true;
|
||||
|
||||
//OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
|
||||
//OnPropertyChanged(nameof(CanSynchronize));
|
||||
|
||||
//SyncFolderCommand?.Execute(null);
|
||||
}
|
||||
|
||||
void IRecipient<MailItemNavigationRequested>.Receive(MailItemNavigationRequested message)
|
||||
{
|
||||
// Find mail item and add to selected items.
|
||||
|
||||
MailItemViewModel navigatingMailItem = null;
|
||||
ThreadMailItemViewModel threadMailItemViewModel = null;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
|
||||
|
||||
if (mailContainer != null)
|
||||
{
|
||||
navigatingMailItem = mailContainer.ItemViewModel;
|
||||
threadMailItemViewModel = mailContainer.ThreadViewModel;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (threadMailItemViewModel != null)
|
||||
threadMailItemViewModel.IsThreadExpanded = true;
|
||||
|
||||
if (navigatingMailItem != null)
|
||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(navigatingMailItem, message.ScrollToItem));
|
||||
else
|
||||
Debugger.Break();
|
||||
}
|
||||
|
||||
async void IRecipient<ActiveMailFolderChangedEvent>.Receive(ActiveMailFolderChangedEvent message)
|
||||
{
|
||||
isChangingFolder = true;
|
||||
|
||||
ActiveFolder = message.BaseFolderMenuItem;
|
||||
|
||||
trackingSynchronizationId = null;
|
||||
completedTrackingSynchronizationCount = 0;
|
||||
|
||||
// Check whether the account synchronizer that this folder belongs to is already in synchronization.
|
||||
await CheckIfAccountIsSynchronizingAsync();
|
||||
|
||||
// Notify change for archive-unarchive app bar button.
|
||||
OnPropertyChanged(nameof(IsArchiveSpecialFolder));
|
||||
|
||||
// Prepare Focused - Other or folder name tabs.
|
||||
UpdateFolderPivots();
|
||||
|
||||
await InitializeFolderAsync();
|
||||
|
||||
// TODO: This should be done in a better way.
|
||||
while (IsInitializingFolder)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
// Let awaiters know about the completion of mail init.
|
||||
message.FolderInitLoadAwaitTask?.TrySetResult(true);
|
||||
|
||||
isChangingFolder = false;
|
||||
}
|
||||
|
||||
void IRecipient<MailItemSelectedEvent>.Receive(MailItemSelectedEvent message)
|
||||
=> SelectedItems.Add(message.SelectedMailItem);
|
||||
|
||||
void IRecipient<MailItemSelectionRemovedEvent>.Receive(MailItemSelectionRemovedEvent message)
|
||||
=> SelectedItems.Remove(message.RemovedMailItem);
|
||||
|
||||
public void Receive(AccountSynchronizationCompleted message)
|
||||
{
|
||||
if (ActiveFolder == null) return;
|
||||
|
||||
bool isLinkedInboxSyncResult = message.SynchronizationTrackingId == trackingSynchronizationId;
|
||||
|
||||
if (isLinkedInboxSyncResult)
|
||||
{
|
||||
var isCompletedAccountListed = ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId);
|
||||
|
||||
if (isCompletedAccountListed) completedTrackingSynchronizationCount++;
|
||||
|
||||
// Group sync is started but not all folders are synchronized yet. Don't report progress.
|
||||
if (completedTrackingSynchronizationCount < ActiveFolder.HandlingFolders.Count()) return;
|
||||
}
|
||||
|
||||
bool isReportingActiveAccountResult = ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId);
|
||||
|
||||
if (!isReportingActiveAccountResult) return;
|
||||
|
||||
// At this point either all folders or a single folder sync is completed.
|
||||
switch (message.Result)
|
||||
{
|
||||
case SynchronizationCompletedState.Success:
|
||||
UpdateBarMessage(InfoBarMessageType.Success, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Success);
|
||||
break;
|
||||
case SynchronizationCompletedState.Failed:
|
||||
UpdateBarMessage(InfoBarMessageType.Error, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Receive(NewSynchronizationRequested message)
|
||||
=> await ExecuteUIThread(() => { OnPropertyChanged(nameof(CanSynchronize)); });
|
||||
|
||||
[RelayCommand]
|
||||
public async Task PerformSearchAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsPerformingSearch = !string.IsNullOrEmpty(SearchQuery);
|
||||
|
||||
await InitializeFolderAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsPerformingSearch = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Receive(AccountSynchronizerStateChanged message)
|
||||
=> await CheckIfAccountIsSynchronizingAsync();
|
||||
|
||||
private async Task CheckIfAccountIsSynchronizingAsync()
|
||||
{
|
||||
bool isAnyAccountSynchronizing = false;
|
||||
|
||||
// Check each account that this page is listing folders from.
|
||||
// If any of the synchronizers are synchronizing, we disable sync.
|
||||
|
||||
if (ActiveFolder != null)
|
||||
{
|
||||
var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId);
|
||||
|
||||
foreach (var accountId in accountIds)
|
||||
{
|
||||
var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(accountId);
|
||||
|
||||
if (synchronizer == null) continue;
|
||||
|
||||
bool isAccountSynchronizing = synchronizer.State != AccountSynchronizerState.Idle;
|
||||
|
||||
if (isAccountSynchronizing)
|
||||
{
|
||||
isAnyAccountSynchronizing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => { IsAccountSynchronizerInSynchronization = isAnyAccountSynchronizing; });
|
||||
}
|
||||
}
|
||||
}
|
||||
619
Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
Normal file
619
Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
Normal file
@@ -0,0 +1,619 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MailKit;
|
||||
using Microsoft.AppCenter.Crashes;
|
||||
using MimeKit;
|
||||
using Serilog;
|
||||
using Wino.Core;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
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.Reader;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Messages.Mails;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class MailRenderingPageViewModel : BaseViewModel,
|
||||
ITransferProgress // For listening IMAP message download progress.
|
||||
{
|
||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly Core.Domain.Interfaces.IMailService _mailService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IWinoSynchronizerFactory _winoSynchronizerFactory;
|
||||
private readonly IWinoRequestDelegator _requestDelegator;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
|
||||
private bool forceImageLoading = false;
|
||||
|
||||
private MailItemViewModel initializedMailItemViewModel = null;
|
||||
private MimeMessageInformation initializedMimeMessageInformation = null;
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
|
||||
public bool CanUnsubscribe => CurrentRenderModel?.CanUnsubscribe ?? false;
|
||||
public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
||||
|
||||
public bool IsImageRenderingDisabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsJunkMail)
|
||||
{
|
||||
return !forceImageLoading;
|
||||
}
|
||||
else
|
||||
{
|
||||
return !CurrentRenderModel?.MailRenderingOptions?.LoadImages ?? false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool isDarkWebviewRenderer;
|
||||
public bool IsDarkWebviewRenderer
|
||||
{
|
||||
get => isDarkWebviewRenderer;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref isDarkWebviewRenderer, value))
|
||||
{
|
||||
InitializeCommandBarItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShouldDisplayDownloadProgress))]
|
||||
private bool isIndetermineProgress;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShouldDisplayDownloadProgress))]
|
||||
private double currentDownloadPercentage;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanUnsubscribe))]
|
||||
private MailRenderModel currentRenderModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string subject;
|
||||
|
||||
[ObservableProperty]
|
||||
private string fromAddress;
|
||||
|
||||
[ObservableProperty]
|
||||
private string fromName;
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime creationDate;
|
||||
|
||||
|
||||
public ObservableCollection<AddressInformation> ToItems { get; set; } = new ObservableCollection<AddressInformation>();
|
||||
public ObservableCollection<AddressInformation> CCItemsItems { get; set; } = new ObservableCollection<AddressInformation>();
|
||||
public ObservableCollection<AddressInformation> BCCItems { get; set; } = new ObservableCollection<AddressInformation>();
|
||||
public ObservableCollection<MailAttachmentViewModel> Attachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>();
|
||||
public ObservableCollection<MailOperationMenuItem> MenuItems { get; set; } = new ObservableCollection<MailOperationMenuItem>();
|
||||
|
||||
#endregion
|
||||
|
||||
public INativeAppService NativeAppService { get; }
|
||||
public IStatePersistanceService StatePersistanceService { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
public MailRenderingPageViewModel(IDialogService dialogService,
|
||||
INativeAppService nativeAppService,
|
||||
IUnderlyingThemeService underlyingThemeService,
|
||||
IMimeFileService mimeFileService,
|
||||
Core.Domain.Interfaces.IMailService mailService,
|
||||
IFileService fileService,
|
||||
IWinoSynchronizerFactory winoSynchronizerFactory,
|
||||
IWinoRequestDelegator requestDelegator,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IClipboardService clipboardService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
{
|
||||
NativeAppService = nativeAppService;
|
||||
StatePersistanceService = statePersistanceService;
|
||||
PreferencesService = preferencesService;
|
||||
|
||||
_clipboardService = clipboardService;
|
||||
_underlyingThemeService = underlyingThemeService;
|
||||
_mimeFileService = mimeFileService;
|
||||
_mailService = mailService;
|
||||
_fileService = fileService;
|
||||
_winoSynchronizerFactory = winoSynchronizerFactory;
|
||||
_requestDelegator = requestDelegator;
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CopyClipboard(string copyText)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _clipboardService.CopyClipboardAsync(copyText);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.ClipboardTextCopied_Title, string.Format(Translator.ClipboardTextCopied_Message, copyText), InfoBarMessageType.Information);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.GeneralTitle_Error, string.Format(Translator.ClipboardTextCopyFailed_Message, copyText), InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ForceImageLoading()
|
||||
{
|
||||
forceImageLoading = true;
|
||||
|
||||
if (initializedMailItemViewModel == null && initializedMimeMessageInformation == null) return;
|
||||
|
||||
if (initializedMailItemViewModel != null)
|
||||
await RenderAsync(initializedMimeMessageInformation);
|
||||
else
|
||||
await RenderAsync(initializedMimeMessageInformation);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task UnsubscribeAsync()
|
||||
{
|
||||
if (!CurrentRenderModel?.CanUnsubscribe ?? false) return;
|
||||
|
||||
// TODO: Support for List-Unsubscribe-Post header. It can be done without launching browser.
|
||||
// https://certified-senders.org/wp-content/uploads/2017/07/CSA_one-click_list-unsubscribe.pdf
|
||||
|
||||
// TODO: Sometimes unsubscribe link can be a mailto: link.
|
||||
// or sometimes with mailto AND http link. We need to handle this.
|
||||
|
||||
if (Uri.IsWellFormedUriString(CurrentRenderModel.UnsubscribeLink, UriKind.RelativeOrAbsolute))
|
||||
await NativeAppService.LaunchUriAsync(new Uri((CurrentRenderModel.UnsubscribeLink)));
|
||||
else
|
||||
DialogService.InfoBarMessage(Translator.Info_UnsubscribeLinkInvalidTitle, Translator.Info_UnsubscribeLinkInvalidMessage, InfoBarMessageType.Error);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OperationClicked(MailOperationMenuItem menuItem)
|
||||
{
|
||||
if (menuItem == null) return;
|
||||
|
||||
await HandleMailOperationAsync(menuItem.Operation);
|
||||
}
|
||||
|
||||
private async Task HandleMailOperationAsync(MailOperation operation)
|
||||
{
|
||||
// Toggle theme
|
||||
if (operation == MailOperation.DarkEditor || operation == MailOperation.LightEditor)
|
||||
IsDarkWebviewRenderer = !IsDarkWebviewRenderer;
|
||||
else if (operation == MailOperation.SaveAs)
|
||||
{
|
||||
// Save as PDF
|
||||
var pickedFolder = await DialogService.PickWindowsFolderAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(pickedFolder))
|
||||
{
|
||||
var fullPath = Path.Combine(pickedFolder, $"{initializedMailItemViewModel.FromAddress}.pdf");
|
||||
Messenger.Send(new SaveAsPDFRequested(fullPath));
|
||||
}
|
||||
}
|
||||
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.Reply)
|
||||
draftOptions.Reason = DraftCreationReason.Reply;
|
||||
else if (operation == MailOperation.ReplyAll)
|
||||
draftOptions.Reason = DraftCreationReason.ReplyAll;
|
||||
else if (operation == MailOperation.Forward)
|
||||
draftOptions.Reason = DraftCreationReason.Forward;
|
||||
|
||||
// TODO: Separate mailto related stuff out of DraftCreationOptions and provide better
|
||||
// model for draft preperation request. Right now it's a mess.
|
||||
|
||||
draftOptions.ReferenceMailCopy = initializedMailItemViewModel.MailCopy;
|
||||
draftOptions.ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage;
|
||||
|
||||
var createdMimeMessage = await _mailService.CreateDraftMimeMessageAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
|
||||
|
||||
var createdDraftMailMessage = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount,
|
||||
createdMimeMessage,
|
||||
initializedMimeMessageInformation.MimeMessage,
|
||||
initializedMailItemViewModel).ConfigureAwait(false);
|
||||
|
||||
var draftPreperationRequest = new DraftPreperationRequest(initializedMailItemViewModel.AssignedAccount, createdDraftMailMessage, createdMimeMessage)
|
||||
{
|
||||
ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage,
|
||||
ReferenceMailCopy = initializedMailItemViewModel.MailCopy
|
||||
};
|
||||
|
||||
await _requestDelegator.ExecuteAsync(draftPreperationRequest);
|
||||
|
||||
}
|
||||
else if (initializedMailItemViewModel != null)
|
||||
{
|
||||
// All other operations require a mail item.
|
||||
var prepRequest = new MailOperationPreperationRequest(operation, initializedMailItemViewModel.MailCopy);
|
||||
await _requestDelegator.ExecuteAsync(prepRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private CancellationTokenSource renderCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
renderCancellationTokenSource.Cancel();
|
||||
|
||||
initializedMailItemViewModel = null;
|
||||
initializedMimeMessageInformation = null;
|
||||
|
||||
// This page can be accessed for 2 purposes.
|
||||
// 1. Rendering a mail item when the user selects.
|
||||
// 2. Rendering an existing EML file with MimeMessage.
|
||||
|
||||
// MimeMessage rendering must be readonly and no command bar items must be shown except common
|
||||
// items like dark/light editor, zoom, print etc.
|
||||
|
||||
// Configure common rendering properties first.
|
||||
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
|
||||
|
||||
await ResetPagePropertiesAsync();
|
||||
|
||||
renderCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Mime content might not be available for now and might require a download.
|
||||
try
|
||||
{
|
||||
if (parameters is MailItemViewModel selectedMailItemViewModel)
|
||||
await RenderAsync(selectedMailItemViewModel, renderCancellationTokenSource.Token);
|
||||
else if (parameters is MimeMessageInformation mimeMessageInformation)
|
||||
await RenderAsync(mimeMessageInformation);
|
||||
|
||||
InitializeCommandBarItems();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Log.Information("Canceled mail rendering.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_MailRenderingFailedTitle, string.Format(Translator.Info_MailRenderingFailedMessage, ex.Message), InfoBarMessageType.Error);
|
||||
|
||||
Crashes.TrackError(ex);
|
||||
Log.Error(ex, "Render Failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
StatePersistanceService.IsReadingMail = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task HandleSingleItemDownloadAsync(MailItemViewModel mailItemViewModel)
|
||||
{
|
||||
var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(mailItemViewModel.AssignedAccount.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// To show the progress on the UI.
|
||||
CurrentDownloadPercentage = 1;
|
||||
|
||||
await synchronizer.DownloadMissingMimeMessageAsync(mailItemViewModel.MailCopy, this, renderCancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Log.Information("MIME download is canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ResetProgress();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
|
||||
|
||||
if (!isMimeExists)
|
||||
{
|
||||
await HandleSingleItemDownloadAsync(mailItemViewModel);
|
||||
}
|
||||
|
||||
// Find the MIME for this item and render it.
|
||||
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
|
||||
mailItemViewModel.AssignedAccount.Id,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (mimeMessageInformation == null)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_MessageCorruptedTitle, Translator.Info_MessageCorruptedMessage, InfoBarMessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
initializedMailItemViewModel = mailItemViewModel;
|
||||
await RenderAsync(mimeMessageInformation);
|
||||
}
|
||||
|
||||
private async Task RenderAsync(MimeMessageInformation mimeMessageInformation)
|
||||
{
|
||||
var message = mimeMessageInformation.MimeMessage;
|
||||
var messagePath = mimeMessageInformation.Path;
|
||||
|
||||
initializedMimeMessageInformation = mimeMessageInformation;
|
||||
|
||||
// TODO: Handle S/MIME decryption.
|
||||
// initializedMimeMessageInformation.MimeMessage.Body is MultipartSigned
|
||||
|
||||
var renderingOptions = PreferencesService.GetRenderingOptions();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
Subject = message.Subject;
|
||||
|
||||
// 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;
|
||||
|
||||
// Extract to,cc and bcc
|
||||
LoadAddressInfo(message.To, ToItems);
|
||||
LoadAddressInfo(message.Cc, CCItemsItems);
|
||||
LoadAddressInfo(message.Bcc, BCCItems);
|
||||
|
||||
// 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)
|
||||
{
|
||||
renderingOptions.LoadImages = false;
|
||||
}
|
||||
|
||||
// Load images if forced.
|
||||
if (forceImageLoading)
|
||||
{
|
||||
renderingOptions.LoadImages = true;
|
||||
}
|
||||
|
||||
CurrentRenderModel = _mimeFileService.GetMailRenderModel(message, messagePath, renderingOptions);
|
||||
|
||||
Messenger.Send(new HtmlRenderingRequested(CurrentRenderModel.RenderHtml));
|
||||
|
||||
foreach (var attachment in CurrentRenderModel.Attachments)
|
||||
{
|
||||
Attachments.Add(new MailAttachmentViewModel(attachment));
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsImageRenderingDisabled));
|
||||
});
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
renderCancellationTokenSource.Cancel();
|
||||
CurrentDownloadPercentage = 0d;
|
||||
|
||||
initializedMailItemViewModel = null;
|
||||
initializedMimeMessageInformation = null;
|
||||
|
||||
StatePersistanceService.IsReadingMail = false;
|
||||
forceImageLoading = false;
|
||||
}
|
||||
|
||||
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is MailboxAddress mailboxAddress)
|
||||
collection.Add(mailboxAddress.ToAddressInformation());
|
||||
else if (item is GroupAddress groupAddress)
|
||||
LoadAddressInfo(groupAddress.Members, collection);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetProgress()
|
||||
{
|
||||
CurrentDownloadPercentage = 0;
|
||||
IsIndetermineProgress = false;
|
||||
}
|
||||
|
||||
private async Task ResetPagePropertiesAsync()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ResetProgress();
|
||||
|
||||
ToItems.Clear();
|
||||
CCItemsItems.Clear();
|
||||
BCCItems.Clear();
|
||||
Attachments.Clear();
|
||||
|
||||
// Dispose existing content first.
|
||||
Messenger.Send(new CancelRenderingContentRequested());
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeCommandBarItems()
|
||||
{
|
||||
MenuItems.Clear();
|
||||
|
||||
// Add light/dark editor theme switch.
|
||||
if (_underlyingThemeService.IsUnderlyingThemeDark())
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.LightEditor));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor));
|
||||
|
||||
// Save As PDF
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SaveAs, true, true));
|
||||
|
||||
if (initializedMailItemViewModel == null)
|
||||
return;
|
||||
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// You can't do these to draft items.
|
||||
if (!initializedMailItemViewModel.IsDraft)
|
||||
{
|
||||
// Reply
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Reply));
|
||||
|
||||
// Reply All
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.ReplyAll));
|
||||
|
||||
// Forward
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Forward));
|
||||
}
|
||||
|
||||
// Archive - Unarchive
|
||||
if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Flag - Clear Flag
|
||||
if (initializedMailItemViewModel.IsFlagged)
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
|
||||
// Secondary items.
|
||||
|
||||
// Read - Unread
|
||||
if (initializedMailItemViewModel.IsRead)
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread, true, false));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
|
||||
}
|
||||
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail)
|
||||
{
|
||||
base.OnMailUpdated(updatedMail);
|
||||
|
||||
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;
|
||||
|
||||
// Mail operation might change the mail item like mark read/unread or change flag.
|
||||
// So we need to update the mail item view model when this happens.
|
||||
// Also command bar items must be re-initialized since the items loaded based on the mail item.
|
||||
|
||||
await ExecuteUIThread(() => { InitializeCommandBarItems(); });
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileFolderPath = Path.Combine(initializedMimeMessageInformation.Path, attachmentViewModel.FileName);
|
||||
var directoryInfo = new DirectoryInfo(initializedMimeMessageInformation.Path);
|
||||
|
||||
var fileExists = File.Exists(fileFolderPath);
|
||||
|
||||
if (!fileExists)
|
||||
await SaveAttachmentInternalAsync(attachmentViewModel, initializedMimeMessageInformation.Path);
|
||||
|
||||
await LaunchFileInternalAsync(fileFolderPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, WinoErrors.OpenAttachment);
|
||||
Crashes.TrackError(ex);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_AttachmentOpenFailedTitle, Translator.Info_AttachmentOpenFailedMessage, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
|
||||
{
|
||||
if (attachmentViewModel == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
attachmentViewModel.IsBusy = true;
|
||||
|
||||
var pickedPath = await DialogService.PickWindowsFolderAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(pickedPath)) return;
|
||||
|
||||
await SaveAttachmentInternalAsync(attachmentViewModel, pickedPath);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_AttachmentSaveSuccessTitle, Translator.Info_AttachmentSaveSuccessMessage, InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, WinoErrors.SaveAttachment);
|
||||
Crashes.TrackError(ex);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_AttachmentSaveFailedTitle, Translator.Info_AttachmentSaveFailedMessage, InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
attachmentViewModel.IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns created file path.
|
||||
private async Task<string> SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath)
|
||||
{
|
||||
var fullFilePath = Path.Combine(saveFolderPath, attachmentViewModel.FileName);
|
||||
var stream = await _fileService.GetFileStreamAsync(saveFolderPath, attachmentViewModel.FileName);
|
||||
|
||||
using (stream)
|
||||
{
|
||||
await attachmentViewModel.MimeContent.DecodeToAsync(stream);
|
||||
}
|
||||
|
||||
return fullFilePath;
|
||||
}
|
||||
|
||||
private async Task LaunchFileInternalAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
await NativeAppService.LaunchFileAsync(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_FileLaunchFailedTitle, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
void ITransferProgress.Report(long bytesTransferred, long totalSize)
|
||||
=> _ = ExecuteUIThread(() => { CurrentDownloadPercentage = bytesTransferred * 100 / Math.Max(1, totalSize); });
|
||||
|
||||
// For upload.
|
||||
void ITransferProgress.Report(long bytesTransferred) { }
|
||||
}
|
||||
}
|
||||
216
Wino.Mail.ViewModels/MergedAccountDetailsPageViewModel.cs
Normal file
216
Wino.Mail.ViewModels/MergedAccountDetailsPageViewModel.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
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.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Messages.Navigation;
|
||||
using Wino.Core.Requests;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class MergedAccountDetailsPageViewModel : BaseViewModel,
|
||||
IRecipient<MergedInboxRenamed>
|
||||
{
|
||||
[ObservableProperty]
|
||||
private MergedAccountProviderDetailViewModel editingMergedAccount;
|
||||
|
||||
[ObservableProperty]
|
||||
private string mergedAccountName;
|
||||
|
||||
public ObservableCollection<AccountProviderDetailViewModel> LinkedAccounts { get; set; } = [];
|
||||
public ObservableCollection<AccountProviderDetailViewModel> UnlinkedAccounts { get; set; } = [];
|
||||
|
||||
// Empty Guid is passed for new created merged inboxes.
|
||||
public bool IsMergedInboxSaved => EditingMergedAccount != null && EditingMergedAccount.MergedInbox.Id != Guid.Empty;
|
||||
|
||||
public bool CanUnlink => IsMergedInboxSaved;
|
||||
|
||||
// There must be at least 2 accounts linked to a merged account for link to exist.
|
||||
public bool ShouldDeleteMergedAccount => LinkedAccounts.Count < 2;
|
||||
|
||||
public bool CanSaveChanges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsMergedInboxSaved)
|
||||
{
|
||||
return ShouldDeleteMergedAccount || IsEditingAccountsDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
return LinkedAccounts.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IProviderService _providerService;
|
||||
|
||||
public MergedAccountDetailsPageViewModel(IDialogService dialogService,
|
||||
IAccountService accountService,
|
||||
IPreferencesService preferencesService,
|
||||
IProviderService providerService) : base(dialogService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_preferencesService = preferencesService;
|
||||
_providerService = providerService;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanUnlink))]
|
||||
private async Task UnlinkAccountsAsync()
|
||||
{
|
||||
if (EditingMergedAccount == null) return;
|
||||
|
||||
var isConfirmed = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_UnlinkAccountsConfirmationMessage, Translator.DialogMessage_UnlinkAccountsConfirmationTitle, Translator.Buttons_Yes);
|
||||
|
||||
if (!isConfirmed) return;
|
||||
|
||||
await _accountService.UnlinkMergedInboxAsync(EditingMergedAccount.MergedInbox.Id);
|
||||
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSaveChanges))]
|
||||
private async Task SaveChangesAsync()
|
||||
{
|
||||
if (ShouldDeleteMergedAccount)
|
||||
{
|
||||
await UnlinkAccountsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsMergedInboxSaved)
|
||||
{
|
||||
await _accountService.UpdateMergedInboxAsync(EditingMergedAccount.MergedInbox.Id, LinkedAccounts.Select(a => a.Account.Id).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
await _accountService.CreateMergeAccountsAsync(EditingMergedAccount.MergedInbox, LinkedAccounts.Select(a => a.Account).ToList());
|
||||
}
|
||||
|
||||
// Startup entity is linked now. Change the startup entity.
|
||||
if (_preferencesService.StartupEntityId != null && LinkedAccounts.Any(a => a.StartupEntityId == _preferencesService.StartupEntityId))
|
||||
{
|
||||
_preferencesService.StartupEntityId = EditingMergedAccount.MergedInbox.Id;
|
||||
}
|
||||
}
|
||||
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RenameLinkAsync()
|
||||
{
|
||||
if (EditingMergedAccount == null) return;
|
||||
|
||||
var newName = await DialogService.ShowTextInputDialogAsync(EditingMergedAccount.MergedInbox.Name,
|
||||
Translator.DialogMessage_RenameLinkedAccountsTitle,
|
||||
Translator.DialogMessage_RenameLinkedAccountsMessage);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName)) return;
|
||||
|
||||
EditingMergedAccount.MergedInbox.Name = newName;
|
||||
|
||||
// Update database record as well.
|
||||
if (IsMergedInboxSaved)
|
||||
{
|
||||
await _accountService.RenameMergedAccountAsync(EditingMergedAccount.MergedInbox.Id, newName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Publish the message manually since the merged inbox is not saved yet.
|
||||
// This is only for breadcrump item update.
|
||||
|
||||
Messenger.Send(new MergedInboxRenamed(EditingMergedAccount.MergedInbox.Id, newName));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void LinkAccount(AccountProviderDetailViewModel account)
|
||||
{
|
||||
LinkedAccounts.Add(account);
|
||||
UnlinkedAccounts.Remove(account);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void UnlinkAccount(AccountProviderDetailViewModel account)
|
||||
{
|
||||
UnlinkedAccounts.Add(account);
|
||||
LinkedAccounts.Remove(account);
|
||||
}
|
||||
|
||||
private bool IsEditingAccountsDirty()
|
||||
{
|
||||
if (EditingMergedAccount == null) return false;
|
||||
|
||||
return EditingMergedAccount.HoldingAccounts.Count != LinkedAccounts.Count ||
|
||||
EditingMergedAccount.HoldingAccounts.Any(a => !LinkedAccounts.Any(la => la.Account.Id == a.Account.Id));
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
LinkedAccounts.CollectionChanged -= LinkedAccountsUpdated;
|
||||
}
|
||||
|
||||
private void LinkedAccountsUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(ShouldDeleteMergedAccount));
|
||||
SaveChangesCommand.NotifyCanExecuteChanged();
|
||||
|
||||
// TODO: Preview common folders for all linked accounts.
|
||||
// Basically showing a preview of how menu items will look.
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
LinkedAccounts.CollectionChanged -= LinkedAccountsUpdated;
|
||||
LinkedAccounts.CollectionChanged += LinkedAccountsUpdated;
|
||||
|
||||
if (parameters is MergedAccountProviderDetailViewModel editingMergedAccount)
|
||||
{
|
||||
MergedAccountName = editingMergedAccount.MergedInbox.Name;
|
||||
EditingMergedAccount = editingMergedAccount;
|
||||
|
||||
foreach (var account in editingMergedAccount.HoldingAccounts)
|
||||
{
|
||||
LinkedAccounts.Add(account);
|
||||
}
|
||||
|
||||
// Load unlinked accounts.
|
||||
|
||||
var allAccounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
foreach (var account in allAccounts)
|
||||
{
|
||||
if (!LinkedAccounts.Any(a => a.Account.Id == account.Id))
|
||||
{
|
||||
var provider = _providerService.GetProviderDetail(account.ProviderType);
|
||||
|
||||
UnlinkedAccounts.Add(new AccountProviderDetailViewModel(provider, account));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnlinkAccountsCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
public void Receive(MergedInboxRenamed message)
|
||||
{
|
||||
if (EditingMergedAccount?.MergedInbox.Id == message.MergedInboxId)
|
||||
{
|
||||
EditingMergedAccount.MergedInbox.Name = message.NewName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
Wino.Mail.ViewModels/MessageListPageViewModel.cs
Normal file
87
Wino.Mail.ViewModels/MessageListPageViewModel.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public class MessageListPageViewModel : BaseViewModel
|
||||
{
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
private List<MailOperation> availableHoverActions = new List<MailOperation>
|
||||
{
|
||||
MailOperation.Archive,
|
||||
MailOperation.SoftDelete,
|
||||
MailOperation.SetFlag,
|
||||
MailOperation.MarkAsRead,
|
||||
MailOperation.MoveToJunk
|
||||
};
|
||||
|
||||
public List<string> AvailableHoverActionsTranslations { get; set; } = new List<string>()
|
||||
{
|
||||
Translator.HoverActionOption_Archive,
|
||||
Translator.HoverActionOption_Delete,
|
||||
Translator.HoverActionOption_ToggleFlag,
|
||||
Translator.HoverActionOption_ToggleRead,
|
||||
Translator.HoverActionOption_MoveJunk
|
||||
};
|
||||
|
||||
#region Properties
|
||||
|
||||
private int leftHoverActionIndex;
|
||||
|
||||
public int LeftHoverActionIndex
|
||||
{
|
||||
get => leftHoverActionIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref leftHoverActionIndex, value))
|
||||
{
|
||||
PreferencesService.LeftHoverAction = availableHoverActions[value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int centerHoverActionIndex;
|
||||
|
||||
public int CenterHoverActionIndex
|
||||
{
|
||||
get => centerHoverActionIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref centerHoverActionIndex, value))
|
||||
{
|
||||
PreferencesService.CenterHoverAction = availableHoverActions[value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int rightHoverActionIndex;
|
||||
|
||||
public int RightHoverActionIndex
|
||||
{
|
||||
get => rightHoverActionIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref rightHoverActionIndex, value))
|
||||
{
|
||||
PreferencesService.RightHoverAction = availableHoverActions[value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public MessageListPageViewModel(IDialogService dialogService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
|
||||
leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction);
|
||||
centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction);
|
||||
rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages
|
||||
{
|
||||
public class ActiveMailFolderChangedEvent : NavigateMailFolderEventArgs
|
||||
{
|
||||
public ActiveMailFolderChangedEvent(IBaseFolderMenuItem baseFolderMenuItem,
|
||||
TaskCompletionSource<bool> folderInitLoadAwaitTask = null) : base(baseFolderMenuItem, folderInitLoadAwaitTask)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Wino.Mail.ViewModels/Messages/ActiveMailItemChangedEvent.cs
Normal file
20
Wino.Mail.ViewModels/Messages/ActiveMailItemChangedEvent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Wino.Core.MenuItems;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// When active mail item in the reader is updated.
|
||||
/// </summary>
|
||||
public class ActiveMailItemChangedEvent
|
||||
{
|
||||
public ActiveMailItemChangedEvent(MailItemViewModel selectedMailItemViewModel)
|
||||
{
|
||||
// SelectedMailItemViewModel can be null.
|
||||
SelectedMailItemViewModel = selectedMailItemViewModel;
|
||||
}
|
||||
|
||||
public MailItemViewModel SelectedMailItemViewModel { get; set; }
|
||||
}
|
||||
}
|
||||
19
Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs
Normal file
19
Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// When a thread conversation listview has single selection, all other listviews
|
||||
/// must unselect all their items.
|
||||
/// </summary>
|
||||
public class ResetSingleMailItemSelectionEvent
|
||||
{
|
||||
public ResetSingleMailItemSelectionEvent(MailItemViewModel selectedViewModel)
|
||||
{
|
||||
SelectedViewModel = selectedViewModel;
|
||||
}
|
||||
|
||||
public MailItemViewModel SelectedViewModel { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
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);
|
||||
}
|
||||
11
Wino.Mail.ViewModels/NewAccountManagementPageViewModel.cs
Normal file
11
Wino.Mail.ViewModels/NewAccountManagementPageViewModel.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public class NewAccountManagementPageViewModel : BaseViewModel
|
||||
{
|
||||
public NewAccountManagementPageViewModel(IDialogService dialogService) : base(dialogService)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
303
Wino.Mail.ViewModels/PersonalizationPageViewModel.cs
Normal file
303
Wino.Mail.ViewModels/PersonalizationPageViewModel.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
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.Personalization;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class PersonalizationPageViewModel : BaseViewModel
|
||||
{
|
||||
public IStatePersistanceService StatePersistanceService { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
private readonly IThemeService _themeService;
|
||||
|
||||
private bool isPropChangeDisabled = false;
|
||||
|
||||
#region Personalization
|
||||
|
||||
public bool IsSelectedWindowsAccentColor => SelectedAppColor == Colors.LastOrDefault();
|
||||
|
||||
public ObservableCollection<AppColorViewModel> Colors { get; set; } = new ObservableCollection<AppColorViewModel>();
|
||||
|
||||
public List<ElementThemeContainer> ElementThemes { get; set; } = new List<ElementThemeContainer>()
|
||||
{
|
||||
new ElementThemeContainer(ApplicationElementTheme.Light, Translator.ElementTheme_Light),
|
||||
new ElementThemeContainer(ApplicationElementTheme.Dark, Translator.ElementTheme_Dark),
|
||||
new ElementThemeContainer(ApplicationElementTheme.Default, Translator.ElementTheme_Default),
|
||||
};
|
||||
|
||||
public List<MailListPaneLengthPreferences> PaneLengths { get; set; } = new List<MailListPaneLengthPreferences>()
|
||||
{
|
||||
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Micro, 300),
|
||||
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Small, 350),
|
||||
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Default, 420),
|
||||
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Medium, 700),
|
||||
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Large, 900),
|
||||
new MailListPaneLengthPreferences(Translator.PaneLengthOption_ExtraLarge, 1200),
|
||||
};
|
||||
|
||||
public List<MailListDisplayMode> InformationDisplayModes { get; set; } = new List<MailListDisplayMode>()
|
||||
{
|
||||
MailListDisplayMode.Compact,
|
||||
MailListDisplayMode.Medium,
|
||||
MailListDisplayMode.Spacious
|
||||
};
|
||||
|
||||
public List<AppThemeBase> AppThemes { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private MailListPaneLengthPreferences selectedMailListPaneLength;
|
||||
|
||||
[ObservableProperty]
|
||||
private ElementThemeContainer selectedElementTheme;
|
||||
|
||||
[ObservableProperty]
|
||||
private MailListDisplayMode selectedInfoDisplayMode;
|
||||
|
||||
private AppColorViewModel _selectedAppColor;
|
||||
|
||||
public AppColorViewModel SelectedAppColor
|
||||
{
|
||||
get => _selectedAppColor;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedAppColor, value))
|
||||
{
|
||||
UseAccentColor = value == Colors?.LastOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _useAccentColor;
|
||||
public bool UseAccentColor
|
||||
{
|
||||
get => _useAccentColor;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useAccentColor, value))
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
SelectedAppColor = Colors?.LastOrDefault();
|
||||
}
|
||||
else if (SelectedAppColor == Colors?.LastOrDefault())
|
||||
{
|
||||
// Unchecking from accent color.
|
||||
|
||||
SelectedAppColor = Colors?.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow app theme change for system themes.
|
||||
public bool CanSelectElementTheme => SelectedAppTheme != null &&
|
||||
(SelectedAppTheme.AppThemeType == AppThemeType.System || SelectedAppTheme.AppThemeType == AppThemeType.Custom);
|
||||
|
||||
private AppThemeBase _selectedAppTheme;
|
||||
|
||||
public AppThemeBase SelectedAppTheme
|
||||
{
|
||||
get => _selectedAppTheme;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedAppTheme, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanSelectElementTheme));
|
||||
|
||||
if (!CanSelectElementTheme)
|
||||
{
|
||||
SelectedElementTheme = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public AsyncRelayCommand CreateCustomThemeCommand { get; set; }
|
||||
public PersonalizationPageViewModel(IDialogService dialogService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IThemeService themeService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
{
|
||||
CreateCustomThemeCommand = new AsyncRelayCommand(CreateCustomThemeAsync);
|
||||
|
||||
StatePersistanceService = statePersistanceService;
|
||||
|
||||
_themeService = themeService;
|
||||
PreferencesService = preferencesService;
|
||||
}
|
||||
|
||||
private async Task CreateCustomThemeAsync()
|
||||
{
|
||||
bool isThemeCreated = await DialogService.ShowCustomThemeBuilderDialogAsync();
|
||||
|
||||
if (isThemeCreated)
|
||||
{
|
||||
// Reload themes.
|
||||
|
||||
await InitializeSettingsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeColors()
|
||||
{
|
||||
Colors.Add(new AppColorViewModel("#0078d7"));
|
||||
Colors.Add(new AppColorViewModel("#00838c"));
|
||||
Colors.Add(new AppColorViewModel("#e3008c"));
|
||||
Colors.Add(new AppColorViewModel("#ca4f07"));
|
||||
Colors.Add(new AppColorViewModel("#e81123"));
|
||||
Colors.Add(new AppColorViewModel("#00819e"));
|
||||
Colors.Add(new AppColorViewModel("#10893e"));
|
||||
Colors.Add(new AppColorViewModel("#881798"));
|
||||
Colors.Add(new AppColorViewModel("#c239b3"));
|
||||
Colors.Add(new AppColorViewModel("#767676"));
|
||||
Colors.Add(new AppColorViewModel("#e1b12c"));
|
||||
Colors.Add(new AppColorViewModel("#16a085"));
|
||||
Colors.Add(new AppColorViewModel("#0984e3"));
|
||||
Colors.Add(new AppColorViewModel("#4a69bd"));
|
||||
Colors.Add(new AppColorViewModel("#05c46b"));
|
||||
|
||||
// Add system accent color as last item.
|
||||
|
||||
Colors.Add(new AppColorViewModel(_themeService.GetSystemAccentColorHex(), true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set selections from settings service.
|
||||
/// </summary>
|
||||
private void SetInitialValues()
|
||||
{
|
||||
SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _themeService.RootTheme);
|
||||
SelectedInfoDisplayMode = PreferencesService.MailItemDisplayMode;
|
||||
SelectedMailListPaneLength = PaneLengths.Find(a => a.Length == StatePersistanceService.MailListPaneLength);
|
||||
|
||||
var currentAccentColor = _themeService.AccentColor;
|
||||
|
||||
bool isWindowsColor = string.IsNullOrEmpty(currentAccentColor);
|
||||
|
||||
if (isWindowsColor)
|
||||
{
|
||||
SelectedAppColor = Colors.LastOrDefault();
|
||||
UseAccentColor = true;
|
||||
}
|
||||
else
|
||||
SelectedAppColor = Colors.FirstOrDefault(a => a.Hex == currentAccentColor);
|
||||
|
||||
SelectedAppTheme = AppThemes.Find(a => a.Id == _themeService.CurrentApplicationThemeId);
|
||||
}
|
||||
|
||||
protected override async void OnActivated()
|
||||
{
|
||||
base.OnActivated();
|
||||
|
||||
await InitializeSettingsAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeSettingsAsync()
|
||||
{
|
||||
Deactivate();
|
||||
|
||||
AppThemes = await _themeService.GetAvailableThemesAsync();
|
||||
|
||||
OnPropertyChanged(nameof(AppThemes));
|
||||
|
||||
InitializeColors();
|
||||
SetInitialValues();
|
||||
|
||||
PropertyChanged -= PersonalizationSettingsUpdated;
|
||||
PropertyChanged += PersonalizationSettingsUpdated;
|
||||
|
||||
_themeService.AccentColorChanged -= AccentColorChanged;
|
||||
_themeService.ElementThemeChanged -= ElementThemeChanged;
|
||||
_themeService.AccentColorChangedBySystem -= AccentColorChangedBySystem;
|
||||
|
||||
_themeService.AccentColorChanged += AccentColorChanged;
|
||||
_themeService.ElementThemeChanged += ElementThemeChanged;
|
||||
_themeService.AccentColorChangedBySystem += AccentColorChangedBySystem;
|
||||
}
|
||||
|
||||
private void AccentColorChangedBySystem(object sender, string newAccentColorHex)
|
||||
{
|
||||
var accentInList = Colors.FirstOrDefault(a => a.IsAccentColor);
|
||||
|
||||
if (accentInList != null)
|
||||
{
|
||||
accentInList.Hex = newAccentColorHex;
|
||||
}
|
||||
}
|
||||
|
||||
private void AccentColorChanged(object sender, string e)
|
||||
{
|
||||
isPropChangeDisabled = true;
|
||||
|
||||
SelectedAppColor = Colors.FirstOrDefault(a => a.Hex == e);
|
||||
|
||||
isPropChangeDisabled = false;
|
||||
}
|
||||
|
||||
private void ElementThemeChanged(object sender, ApplicationElementTheme e)
|
||||
{
|
||||
isPropChangeDisabled = true;
|
||||
|
||||
SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == e);
|
||||
|
||||
isPropChangeDisabled = false;
|
||||
}
|
||||
|
||||
protected override void OnDeactivated()
|
||||
{
|
||||
base.OnDeactivated();
|
||||
|
||||
Deactivate();
|
||||
}
|
||||
|
||||
private void Deactivate()
|
||||
{
|
||||
PropertyChanged -= PersonalizationSettingsUpdated;
|
||||
|
||||
_themeService.AccentColorChanged -= AccentColorChanged;
|
||||
_themeService.ElementThemeChanged -= ElementThemeChanged;
|
||||
_themeService.AccentColorChangedBySystem -= AccentColorChangedBySystem;
|
||||
|
||||
if (AppThemes != null)
|
||||
{
|
||||
AppThemes.Clear();
|
||||
AppThemes = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void PersonalizationSettingsUpdated(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (isPropChangeDisabled)
|
||||
return;
|
||||
|
||||
if (e.PropertyName == nameof(SelectedElementTheme) && SelectedElementTheme != null)
|
||||
{
|
||||
_themeService.RootTheme = SelectedElementTheme.NativeTheme;
|
||||
}
|
||||
else if (e.PropertyName == nameof(SelectedAppTheme))
|
||||
{
|
||||
_themeService.CurrentApplicationThemeId = SelectedAppTheme.Id;
|
||||
}
|
||||
else if (e.PropertyName == nameof(SelectedMailListPaneLength) && SelectedMailListPaneLength != null)
|
||||
StatePersistanceService.MailListPaneLength = SelectedMailListPaneLength.Length;
|
||||
else
|
||||
{
|
||||
if (e.PropertyName == nameof(SelectedInfoDisplayMode))
|
||||
PreferencesService.MailItemDisplayMode = SelectedInfoDisplayMode;
|
||||
else if (e.PropertyName == nameof(SelectedAppColor))
|
||||
_themeService.AccentColor = SelectedAppColor.Hex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Wino.Mail.ViewModels/ReadingPanePageViewModel.cs
Normal file
77
Wino.Mail.ViewModels/ReadingPanePageViewModel.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class ReadingPanePageViewModel : BaseViewModel,
|
||||
IRecipient<PropertyChangedMessage<ReaderFontModel>>,
|
||||
IRecipient<PropertyChangedMessage<int>>
|
||||
{
|
||||
public IPreferencesService PreferencesService { get; set; }
|
||||
|
||||
private int selectedMarkAsOptionIndex;
|
||||
private readonly IFontService _fontService;
|
||||
|
||||
public int SelectedMarkAsOptionIndex
|
||||
{
|
||||
get => selectedMarkAsOptionIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedMarkAsOptionIndex, value))
|
||||
{
|
||||
if (value >= 0)
|
||||
{
|
||||
PreferencesService.MarkAsPreference = (MailMarkAsOption)Enum.GetValues(typeof(MailMarkAsOption)).GetValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<ReaderFontModel> ReaderFonts => _fontService.GetReaderFonts();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedRecipients]
|
||||
ReaderFontModel currentReaderFont;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedRecipients]
|
||||
int currentReaderFontSize;
|
||||
|
||||
public ReadingPanePageViewModel(IDialogService dialogService,
|
||||
IFontService fontService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
{
|
||||
_fontService = fontService;
|
||||
|
||||
PreferencesService = preferencesService;
|
||||
SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues(typeof(MailMarkAsOption)), PreferencesService.MarkAsPreference);
|
||||
|
||||
CurrentReaderFont = fontService.GetCurrentReaderFont();
|
||||
CurrentReaderFontSize = fontService.GetCurrentReaderFontSize();
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<ReaderFontModel> message)
|
||||
{
|
||||
if (message.OldValue != message.NewValue)
|
||||
{
|
||||
_fontService.ChangeReaderFont(message.NewValue.Font);
|
||||
Debug.WriteLine("Changed reader font.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<int> message)
|
||||
{
|
||||
if (message.PropertyName == nameof(CurrentReaderFontSize))
|
||||
{
|
||||
_fontService.ChangeReaderFontSize(CurrentReaderFontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Wino.Mail.ViewModels/SettingOptionsPageViewModel.cs
Normal file
85
Wino.Mail.ViewModels/SettingOptionsPageViewModel.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Translations;
|
||||
using Wino.Core.Messages.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class SettingOptionsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ITranslationService _translationService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
|
||||
[ObservableProperty]
|
||||
private List<AppLanguageModel> _availableLanguages;
|
||||
|
||||
[ObservableProperty]
|
||||
private AppLanguageModel _selectedLanguage;
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
public SettingOptionsPageViewModel(IDialogService dialogService,
|
||||
ITranslationService translationService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
{
|
||||
_translationService = translationService;
|
||||
_preferencesService = preferencesService;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoAccountSettings() => Messenger.Send<NavigateSettingsRequested>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void NavigateSubDetail(object type)
|
||||
{
|
||||
if (type is string stringParameter)
|
||||
{
|
||||
WinoPage pageType = default;
|
||||
|
||||
string pageTitle = stringParameter;
|
||||
|
||||
// They are just params and don't have to be localized. Don't change.
|
||||
if (stringParameter == "Personalization")
|
||||
pageType = WinoPage.PersonalizationPage;
|
||||
else if (stringParameter == "About")
|
||||
pageType = WinoPage.AboutPage;
|
||||
else if (stringParameter == "Message List")
|
||||
pageType = WinoPage.MessageListPage;
|
||||
else if (stringParameter == "Reading Pane")
|
||||
pageType = WinoPage.ReadingPanePage;
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(pageTitle, pageType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Wino.Mail.ViewModels/SettingsPageViewModel.cs
Normal file
11
Wino.Mail.ViewModels/SettingsPageViewModel.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public class SettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
public SettingsPageViewModel(IDialogService dialogService) : base(dialogService)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Wino.Mail.ViewModels/SettingsViewModel.cs
Normal file
11
Wino.Mail.ViewModels/SettingsViewModel.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public class SettingsDialogViewModel : BaseViewModel
|
||||
{
|
||||
public SettingsDialogViewModel(IDialogService dialogService) : base(dialogService)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Wino.Mail.ViewModels/SignatureManagementPageViewModel.cs
Normal file
93
Wino.Mail.ViewModels/SignatureManagementPageViewModel.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
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;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Core.Messages.Mails;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class SignatureManagementPageViewModel : BaseViewModel
|
||||
{
|
||||
public Func<Task<string>> GetHTMLBodyFunction;
|
||||
public Func<Task<string>> GetTextBodyFunction;
|
||||
|
||||
public List<EditorToolbarSection> ToolbarSections { get; set; } = new List<EditorToolbarSection>()
|
||||
{
|
||||
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format },
|
||||
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert },
|
||||
};
|
||||
|
||||
[ObservableProperty]
|
||||
private EditorToolbarSection selectedToolbarSection;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSignatureEnabled;
|
||||
|
||||
public MailAccount Account { get; set; }
|
||||
|
||||
public AsyncRelayCommand SaveSignatureCommand { get; set; }
|
||||
|
||||
public INativeAppService NativeAppService { get; }
|
||||
private readonly ISignatureService _signatureService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public SignatureManagementPageViewModel(IDialogService dialogService,
|
||||
INativeAppService nativeAppService,
|
||||
ISignatureService signatureService,
|
||||
IAccountService accountService) : base(dialogService)
|
||||
{
|
||||
SelectedToolbarSection = ToolbarSections[0];
|
||||
NativeAppService = nativeAppService;
|
||||
_signatureService = signatureService;
|
||||
_accountService = accountService;
|
||||
SaveSignatureCommand = new AsyncRelayCommand(SaveSignatureAsync);
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters is Guid accountId)
|
||||
Account = await _accountService.GetAccountAsync(accountId);
|
||||
|
||||
if (Account != null)
|
||||
{
|
||||
var accountSignature = await _signatureService.GetAccountSignatureAsync(Account.Id);
|
||||
|
||||
IsSignatureEnabled = accountSignature != null;
|
||||
|
||||
if (IsSignatureEnabled)
|
||||
Messenger.Send(new HtmlRenderingRequested(accountSignature.HtmlBody));
|
||||
else
|
||||
Messenger.Send(new HtmlRenderingRequested(string.Empty)); // To get the theme changes. Render empty html.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSignatureAsync()
|
||||
{
|
||||
if (IsSignatureEnabled)
|
||||
{
|
||||
var newSignature = Regex.Unescape(await GetHTMLBodyFunction());
|
||||
|
||||
await _signatureService.UpdateAccountSignatureAsync(Account.Id, newSignature);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_SignatureSavedTitle, Translator.Info_SignatureSavedMessage, Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _signatureService.DeleteAccountSignatureAssignment(Account.Id);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_SignatureDisabledTitle, Translator.Info_SignatureDisabledMessage, Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Wino.Mail.ViewModels/WelcomePageViewModel.cs
Normal file
37
Wino.Mail.ViewModels/WelcomePageViewModel.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
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 : BaseViewModel
|
||||
{
|
||||
public const string VersionFile = "170.md";
|
||||
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string currentVersionNotes;
|
||||
|
||||
public WelcomePageViewModel(IDialogService dialogService, IFileService fileService) : base(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
Normal file
22
Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IsExternalInit" Version="1.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="5.0.3" />
|
||||
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user