using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using MoreLinq; using Nito.AsyncEx; using Serilog; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; 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.Domain.Models.Synchronization; using Wino.Core.Services; using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Helpers; using Wino.Mail.ViewModels.Messages; using Wino.Messaging.Client.Mails; using Wino.Messaging.Server; using Wino.Messaging.UI; namespace Wino.Mail.ViewModels; public partial class MailListPageViewModel : MailBaseViewModel, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient { private bool isChangingFolder = false; private Guid? trackingSynchronizationId = null; private int completedTrackingSynchronizationCount = 0; /* [Bug] Unread folder reads All emails automatically with setting "Mark as Read: When Selected" enabled * https://github.com/bkaankose/Wino-Mail/issues/162 * We store the UniqueIds of the mails that are marked as read in Gmail Unread folder * to prevent them from being removed from the list when they are marked as read. */ private readonly HashSet gmailUnreadFolderMarkedAsReadUniqueIds = []; private ThrottledEventHandler _selectionChangedThrottler; public event EventHandler ThrottledSelectionChanged; public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection(); //public ObservableCollection SelectedItems { get; set; } = []; public ObservableCollection PivotFolders { get; set; } = []; public ObservableCollection ActionItems { get; set; } = []; private readonly SemaphoreSlim listManipulationSemepahore = new SemaphoreSlim(1); private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource(); public INavigationService NavigationService { get; } public IStatePersistanceService StatePersistenceService { get; } public IPreferencesService PreferencesService { get; } public INewThemeService ThemeService { get; } private readonly IAccountService _accountService; private readonly IMailDialogService _mailDialogService; private readonly IMailService _mailService; private readonly IFolderService _folderService; private readonly IContextMenuItemService _contextMenuItemService; private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IKeyPressService _keyPressService; private readonly IWinoLogger _winoLogger; private MailItemViewModel _activeMailItem; public List SortingOptions { get; } = [ new(Translator.SortingOption_Date, SortingOptionType.ReceiveDate), new(Translator.SortingOption_Name, SortingOptionType.Sender), ]; public List FilterOptions { get; } = [ new (Translator.FilteringOption_All, FilterOptionType.All), new (Translator.FilteringOption_Unread, FilterOptionType.Unread), new (Translator.FilteringOption_Flagged, FilterOptionType.Flagged), new (Translator.FilteringOption_Files, FilterOptionType.Files) ]; private FolderPivotViewModel _selectedFolderPivot; [ObservableProperty] private bool isMultiSelectionModeEnabled; [ObservableProperty] public partial string SearchQuery { get; set; } [ObservableProperty] private FilterOption _selectedFilterOption; private SortingOption _selectedSortingOption; // Indicates state when folder is initializing. It can happen after folder navigation, search or filter change applied or loading more items. [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsEmpty))] [NotifyPropertyChangedFor(nameof(IsFolderEmpty))] [NotifyPropertyChangedFor(nameof(IsProgressRing))] private bool isInitializingFolder; [ObservableProperty] private InfoBarMessageType barSeverity; [ObservableProperty] private string barMessage; [ObservableProperty] private double mailListLength = 420; [ObservableProperty] private double maxMailListLength = 1200; [ObservableProperty] private string barTitle; [ObservableProperty] private bool isBarOpen; /// /// Current folder that is being represented from the menu. /// [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronize))] [NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))] private IBaseFolderMenuItem activeFolder; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronize))] private bool isAccountSynchronizerInSynchronization; public MailListPageViewModel(IMailDialogService dialogService, INavigationService navigationService, IAccountService accountService, IMailDialogService mailDialogService, IMailService mailService, IStatePersistanceService statePersistenceService, IFolderService folderService, IContextMenuItemService contextMenuItemService, IWinoRequestDelegator winoRequestDelegator, IKeyPressService keyPressService, IPreferencesService preferencesService, INewThemeService themeService, IWinoLogger winoLogger) { PreferencesService = preferencesService; ThemeService = themeService; _winoLogger = winoLogger; StatePersistenceService = statePersistenceService; NavigationService = navigationService; _accountService = accountService; _mailDialogService = mailDialogService; _mailService = mailService; _folderService = folderService; _contextMenuItemService = contextMenuItemService; _winoRequestDelegator = winoRequestDelegator; _keyPressService = keyPressService; SelectedFilterOption = FilterOptions[0]; SelectedSortingOption = SortingOptions[0]; mailListLength = statePersistenceService.MailListPaneLength; _selectionChangedThrottler = new ThrottledEventHandler(100, () => { _ = ExecuteUIThread(() => { if (MailCollection.SelectedVisibleCount == 1) { ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(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(); SetupTopBarActions(); }); ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty); }); } public override void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); MailCollection.SelectionChanged += SelectedItemsChanged; } public override void OnNavigatedFrom(NavigationMode mode, object parameters) { base.OnNavigatedFrom(mode, parameters); MailCollection.SelectionChanged -= SelectedItemsChanged; MailCollection.Dispose(); _selectionChangedThrottler?.Dispose(); _selectionChangedThrottler = null; } private void SelectedItemsChanged(object sender, EventArgs e) { _selectionChangedThrottler?.Trigger(); } private void SetupTopBarActions() { ActionItems.Clear(); var actions = GetAvailableMailActions(MailCollection.SelectedVisibleItems); actions.ForEach(a => ActionItems.Add(a)); } #region Properties /// /// Selected internal folder. This can be either folder's own name or Focused-Other. /// public FolderPivotViewModel SelectedFolderPivot { get => _selectedFolderPivot; set { if (_selectedFolderPivot != null) _selectedFolderPivot.SelectedItemCount = 0; SetProperty(ref _selectedFolderPivot, value); } } /// /// Selected sorting option. /// public SortingOption SelectedSortingOption { get => _selectedSortingOption; set { if (SetProperty(ref _selectedSortingOption, value)) { if (value != null && MailCollection != null) { MailCollection.GroupingType = value.Type == SortingOptionType.ReceiveDate ? EmailGroupingType.ByDate : EmailGroupingType.ByFromName; } } } } public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled; public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false; public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive; public string SelectedMessageText => MailCollection.SelectedVisibleCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedVisibleCount) : Translator.NoMailSelected; /// /// Indicates current state of the mail list. Doesn't matter it's loading or no. /// public bool IsEmpty => MailCollection.Count == 0; /// /// Progress ring only should be visible when the folder is initializing and there are no items. We don't need to show it when there are items. /// public bool IsProgressRing => IsInitializingFolder && IsEmpty; public bool IsFolderEmpty => !IsInitializingFolder && IsEmpty; public bool HasNoOnlineSearchResult { get; private set; } [ObservableProperty] public partial bool IsInSearchMode { get; set; } [ObservableProperty] public partial bool IsOnlineSearchButtonVisible { get; set; } [ObservableProperty] public partial bool IsOnlineSearchEnabled { get; set; } [ObservableProperty] public partial bool AreSearchResultsOnline { get; set; } #endregion private async void ActiveMailItemChanged(MailItemViewModel selectedMailItemViewModel) { if (_activeMailItem == selectedMailItemViewModel) return; // Don't update active mail item if Ctrl key is pressed or multi selection is enabled. // 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(); bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled; if (isMultiSelecting && StatePersistenceService.IsReaderNarrowed) { return; } _activeMailItem = selectedMailItemViewModel; Messenger.Send(new ActiveMailItemChangedEvent(_activeMailItem)); if (_activeMailItem == null || _activeMailItem.IsRead) return; // Automatically set mark as read or not based on preferences. var markAsPreference = PreferencesService.MarkAsPreference; if (markAsPreference == MailMarkAsOption.WhenSelected) { //var operation = MailOperation.MarkAsRead; //var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy); //if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread && // !gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId)) //{ // gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId); //} //await ExecuteMailOperationAsync(package); } else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0) { // TODO: Start a timer then queue. } } public void NotifyItemSelected() { OnPropertyChanged(nameof(SelectedMessageText)); //OnPropertyChanged(nameof(HasSingleItemSelection)); //OnPropertyChanged(nameof(HasSelectedItems)); //OnPropertyChanged(nameof(SelectedItemCount)); //OnPropertyChanged(nameof(HasMultipleItemSelections)); SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount; } private void NotifyItemFoundState() { OnPropertyChanged(nameof(IsEmpty)); OnPropertyChanged(nameof(IsFolderEmpty)); } private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message) { await ExecuteUIThread(() => { BarSeverity = severity; BarTitle = title; BarMessage = message; IsBarOpen = true; }); } private async Task UpdateFolderPivotsAsync() { if (ActiveFolder == null) return; PivotFolders.Clear(); SelectedFolderPivot = null; if (IsInSearchMode) { var isFocused = SelectedFolderPivot?.IsFocused; PivotFolders.Add(new FolderPivotViewModel(Translator.SearchPivotName, isFocused)); } else { // 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 isFocusedInboxEnabled = await _accountService.IsAccountFocusedEnabledAsync(parentAccount.Id); 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(); } #region Commands [RelayCommand] public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request); [RelayCommand] private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem) { if (menuItem == null || MailCollection.SelectedVisibleCount == 0) return; await HandleMailOperation(menuItem.Operation, MailCollection.SelectedVisibleItems); } /// /// Executes the requested mail operation for currently selected items. /// /// Action to execute for selected items. [RelayCommand] private async Task ExecuteMailOperation(MailOperation mailOperation) { if (!MailCollection.SelectedVisibleItems.Any()) return; await HandleMailOperation(mailOperation, MailCollection.SelectedVisibleItems); } private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable mailItems) { if (!mailItems.Any()) return; var package = new MailOperationPreperationRequest(mailOperation, mailItems.Select(a => a.MailCopy)); await ExecuteMailOperationAsync(package); } /// /// Sens a new message to synchronize current folder. /// [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 MailSynchronizationOptions() { AccountId = folder.MailAccountId, Type = MailSynchronizationType.CustomFolders, SynchronizationFolderIds = [folder.Id], GroupedSynchronizationTrackingId = trackingSynchronizationId }; Messenger.Send(new NewMailSynchronizationRequested(options)); } } [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(); } [RelayCommand] public async Task PerformSearchAsync() { IsOnlineSearchEnabled = false; AreSearchResultsOnline = false; IsInSearchMode = !string.IsNullOrEmpty(SearchQuery); if (IsInSearchMode) { IsOnlineSearchButtonVisible = false; } await UpdateFolderPivotsAsync(); } [RelayCommand] private async Task EnableFolderSynchronizationAsync() { if (ActiveFolder == null) return; foreach (var folder in ActiveFolder.HandlingFolders) { await _folderService.ChangeFolderSynchronizationStateAsync(folder.Id, true); } } [RelayCommand] private async Task LoadMoreItemsAsync() { if (IsInitializingFolder || IsOnlineSearchEnabled) return; await ExecuteUIThread(() => { IsInitializingFolder = true; }); var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders, SelectedFilterOption.Type, SelectedSortingOption.Type, PreferencesService.IsThreadingEnabled, SelectedFolderPivot.IsFocused, IsInSearchMode ? SearchQuery : string.Empty, MailCollection.MailCopyIdHashSet); var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false); var viewModels = PrepareMailViewModels(items); await ExecuteUIThread(() => { MailCollection.AddEmails(viewModels); }); await ExecuteUIThread(() => { IsInitializingFolder = false; }); } #endregion public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package); public IEnumerable GetAvailableMailActions(IEnumerable contextMailItems) => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy)); private bool ShouldPreventItemAdd(MailCopy mailItem) { bool condition = mailItem.IsRead && SelectedFilterOption.Type == FilterOptionType.Unread || !mailItem.IsFlagged && SelectedFilterOption.Type == FilterOptionType.Flagged; return condition; } protected override async void OnMailAdded(MailCopy addedMail) { base.OnMailAdded(addedMail); if (addedMail.AssignedAccount == null || addedMail.AssignedFolder == null) return; try { if (ActiveFolder == null) return; // At least one of the accounts we are listing must match with the account of the added mail. if (!ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == addedMail.AssignedAccount.Id)) 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; // Item should be prevented from being added to the list due to filter. if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return; await listManipulationSemepahore.WaitAsync(); await ExecuteUIThread(() => { MailCollection.AddEmail(new MailItemViewModel(addedMail)); NotifyItemFoundState(); }); } catch { } finally { listManipulationSemepahore.Release(); } } protected override async void OnMailUpdated(MailCopy updatedMail) { base.OnMailUpdated(updatedMail); Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); // TODO // await MailCollection.UpdateMailCopy(updatedMail); await ExecuteUIThread(() => { SetupTopBarActions(); }); } protected override async void OnMailRemoved(MailCopy removedMail) { base.OnMailRemoved(removedMail); if (removedMail.AssignedAccount == null || removedMail.AssignedFolder == null) return; // We should delete the items only if: // 1. They are deleted from the active folder. // 2. Deleted from draft or sent folder. // 3. Removal is not caused by Gmail Unread folder action. // 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; bool isDeletedByGmailUnreadFolderAction = ActiveFolder.SpecialFolderType == SpecialFolderType.Unread && gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId); if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction) { bool isDeletedMailSelected = MailCollection.SelectedVisibleItems.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) { await ExecuteUIThread(() => { nextItem = MailCollection.GetNextItem(removedMail); }); } // Remove the deleted item from the list. await ExecuteUIThread(() => { MailCollection.RemoveEmailByMailCopy(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. await ExecuteUIThread(() => { MailCollection.ClearSelections(); }); } await ExecuteUIThread(() => { NotifyItemFoundState(); }); } else if (isDeletedByGmailUnreadFolderAction) { // Remove the entry from the set so we can listen to actual deletes next time. gmailUnreadFolderMarkedAsReadUniqueIds.Remove(removedMail.UniqueId); } } 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(); await ExecuteUIThread(() => { // Create the item. Draft folder navigation is already done at this point. MailCollection.AddEmail(new MailItemViewModel(draftMail)); // New draft is created by user. Select the item. Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true)); NotifyItemFoundState(); }); } finally { listManipulationSemepahore.Release(); } } private IEnumerable PrepareMailViewModels(IEnumerable mailItems) { return mailItems.Select(a => new MailItemViewModel(a)); } [RelayCommand] private async Task PerformOnlineSearchAsync() { IsOnlineSearchButtonVisible = false; IsOnlineSearchEnabled = true; await InitializeFolderAsync(); } private async Task InitializeFolderAsync() { if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null) return; try { MailCollection.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 (!listManipulationCancellationTokenSource.IsCancellationRequested) { listManipulationCancellationTokenSource.Cancel(); } listManipulationCancellationTokenSource = new CancellationTokenSource(); var cancellationToken = listManipulationCancellationTokenSource.Token; await listManipulationSemepahore.WaitAsync(cancellationToken); // Here items are sorted and filtered. List items = null; bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery); bool isDoingOnlineSearch = false; if (isDoingSearch) { isDoingOnlineSearch = PreferencesService.DefaultSearchMode == SearchMode.Online || IsOnlineSearchEnabled; // Perform online search. if (isDoingOnlineSearch) { // TODO: Burak: Handle online search. //WinoServerResponse onlineSearchResult = null; //string onlineSearchFailedMessage = null; //try //{ // var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId).ToList(); // var folders = ActiveFolder.HandlingFolders.ToList(); // var searchRequest = new OnlineSearchRequested(accountIds, SearchQuery, folders); // onlineSearchResult = await _winoServerConnectionManager.GetResponseAsync(searchRequest, cancellationToken); // if (onlineSearchResult.IsSuccess) // { // await ExecuteUIThread(() => { AreSearchResultsOnline = true; }); // onlineSearchItems = onlineSearchResult.Data.SearchResult; // } // else // { // onlineSearchFailedMessage = onlineSearchResult.Message; // } //} //catch (OperationCanceledException) //{ // throw; //} //catch (Exception ex) //{ // Log.Warning(ex, "Failed to perform online search."); // onlineSearchFailedMessage = ex.Message; //} //if (onlineSearchResult != null && !onlineSearchResult.IsSuccess) //{ // // Query or server error. // var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchResult.Message); // _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); //} //else if (!string.IsNullOrEmpty(onlineSearchFailedMessage)) //{ // // Fatal error. // var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchFailedMessage); // _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); //} } } var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders, SelectedFilterOption.Type, SelectedSortingOption.Type, PreferencesService.IsThreadingEnabled, SelectedFolderPivot.IsFocused, SearchQuery, MailCollection.MailCopyIdHashSet); items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false); if (!listManipulationCancellationTokenSource.IsCancellationRequested) { // 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.AddEmails(viewModels); if (isDoingSearch && !isDoingOnlineSearch) { IsOnlineSearchButtonVisible = true; } }); } } catch (OperationCanceledException) { Debug.WriteLine("Initialization of mails canceled."); } catch (Exception ex) { Debugger.Break(); if (IsInSearchMode) Log.Error(ex, "Failed to perform search."); else Log.Error(ex, "Failed to refresh listed mails."); } finally { listManipulationSemepahore.Release(); await ExecuteUIThread(() => { IsInitializingFolder = false; OnPropertyChanged(nameof(CanSynchronize)); NotifyItemFoundState(); }); } } #region Receivers async void IRecipient.Receive(ActiveMailFolderChangedEvent message) { NotifyItemSelected(); isChangingFolder = true; ActiveFolder = message.BaseFolderMenuItem; gmailUnreadFolderMarkedAsReadUniqueIds.Clear(); trackingSynchronizationId = null; completedTrackingSynchronizationCount = 0; // Notify change for archive-unarchive app bar button. OnPropertyChanged(nameof(IsArchiveSpecialFolder)); IsInSearchMode = false; IsOnlineSearchButtonVisible = false; AreSearchResultsOnline = false; // Prepare Focused - Other or folder name tabs. await UpdateFolderPivotsAsync(); // Reset filters and sorting options. ResetFilters(); await InitializeFolderAsync(); // TODO: This should be done in a better way. while (IsInitializingFolder) { await Task.Delay(100); } // Check whether the account synchronizer that this folder belongs to is already in synchronization. await CheckIfAccountIsSynchronizingAsync(); // Let awaiters know about the completion of mail init. message.FolderInitLoadAwaitTask?.TrySetResult(true); await Task.Yield(); isChangingFolder = false; void ResetFilters() { // Expected that FilterOptions and SortingOptions have default value in 0 index. SelectedFilterOption = FilterOptions[0]; SelectedSortingOption = SortingOptions[0]; SearchQuery = string.Empty; IsInSearchMode = false; IsOnlineSearchEnabled = false; } } 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; } } void IRecipient.Receive(MailItemNavigationRequested message) { Debug.WriteLine($"Mail item navigation requested"); // 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)); } #endregion public async void Receive(NewMailSynchronizationRequested message) => await ExecuteUIThread(() => { OnPropertyChanged(nameof(CanSynchronize)); }); protected override async void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { if (ActiveFolder?.EntityId != mailItemFolder.Id) return; await ExecuteUIThread(() => { ActiveFolder.UpdateFolder(mailItemFolder); OnPropertyChanged(nameof(CanSynchronize)); OnPropertyChanged(nameof(IsFolderSynchronizationEnabled)); }); SyncFolderCommand?.Execute(null); } 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) { if (SynchronizationManager.Instance.IsAccountSynchronizing(accountId)) { isAnyAccountSynchronizing = true; break; } } } await ExecuteUIThread(() => { IsAccountSynchronizerInSynchronization = isAnyAccountSynchronizing; }); } public void Receive(AccountCacheResetMessage message) { if (message.Reason == AccountCacheResetReason.ExpiredCache && ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId)) { var handlingFolder = ActiveFolder.HandlingFolders.FirstOrDefault(a => a.MailAccountId == message.AccountId); if (handlingFolder == null) return; _ = ExecuteUIThread(() => { MailCollection.Clear(); _mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning); }); } } public void Receive(ThumbnailAdded message) { Dispatcher.ExecuteOnUIThread(() => { MailCollection.UpdateThumbnailsForAddress(message.Email); }); } protected override void RegisterRecipients() { base.RegisterRecipients(); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); } protected override void UnregisterRecipients() { base.UnregisterRecipients(); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); } }