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 CommunityToolkit.Mvvm.Messaging.Messages; 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.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, 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 = []; public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection(); 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 INotificationBuilder _notificationBuilder; 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))] [NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))] public partial bool IsInitializingFolder { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))] public partial bool FinishedLoading { get; set; } = false; public bool CanLoadMoreItems => !IsInitializingFolder && !IsOnlineSearchEnabled && !FinishedLoading; [ObservableProperty] public partial InfoBarMessageType BarSeverity { get; set; } [ObservableProperty] public partial string BarMessage { get; set; } [ObservableProperty] public partial double MailListLength { get; set; } = 420; [ObservableProperty] public partial double MaxMailListLength { get; set; } = 1200; [ObservableProperty] public partial string BarTitle { get; set; } [ObservableProperty] public partial bool IsBarOpen { get; set; } /// /// Current folder that is being represented from the menu. /// [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronize))] [NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))] public partial IBaseFolderMenuItem ActiveFolder { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanSynchronize))] public partial bool IsAccountSynchronizerInSynchronization { get; set; } public MailListPageViewModel(IMailDialogService dialogService, INavigationService navigationService, IAccountService accountService, IMailDialogService mailDialogService, IMailService mailService, IStatePersistanceService statePersistenceService, INotificationBuilder notificationBuilder, IFolderService folderService, IContextMenuItemService contextMenuItemService, IWinoRequestDelegator winoRequestDelegator, IKeyPressService keyPressService, IPreferencesService preferencesService, INewThemeService themeService, IWinoLogger winoLogger) { _winoLogger = winoLogger; _accountService = accountService; _mailDialogService = mailDialogService; _mailService = mailService; _folderService = folderService; _contextMenuItemService = contextMenuItemService; _winoRequestDelegator = winoRequestDelegator; _keyPressService = keyPressService; PreferencesService = preferencesService; ThemeService = themeService; StatePersistenceService = statePersistenceService; _notificationBuilder = notificationBuilder; NavigationService = navigationService; 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.ItemSelectionChanged += MailItemSelectionChanged; } public override async void OnNavigatedFrom(NavigationMode mode, object parameters) { base.OnNavigatedFrom(mode, parameters); MailCollection.ItemSelectionChanged -= MailItemSelectionChanged; await MailCollection.ClearAsync(); MailCollection.Cleanup(); } private void MailItemSelectionChanged(object sender, EventArgs e) { if (MailCollection.HasSingleItemSelected) { var selectedItem = MailCollection.SelectedItems.ElementAtOrDefault(0); ActiveMailItemChanged(selectedItem); } else if (MailCollection.SelectedItemsCount == 0) { ActiveMailItemChanged(null); } NotifyItemFoundState(); NotifyItemSelected(); SetupTopBarActions(); } private void SetupTopBarActions() { ActionItems.Clear(); var actions = GetAvailableMailActions(MailCollection.SelectedItems); 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.SelectedItemsCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedItemsCount) : Translator.NoMailSelected; /// /// Indicates current state of the mail list. Doesn't matter it's loading or no. /// public bool IsEmpty => MailCollection.AllItemsCount == 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] [NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))] public partial bool IsOnlineSearchEnabled { get; set; } [ObservableProperty] public partial bool AreSearchResultsOnline { get; set; } #endregion private async void ActiveMailItemChanged(MailItemViewModel selectedMailItemViewModel) { if (_activeMailItem == selectedMailItemViewModel) 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)); 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.SelectedItemsCount == 0) return; await HandleMailOperation(menuItem.Operation, MailCollection.SelectedItems); } /// /// Executes the requested mail operation for currently selected items. /// /// Action to execute for selected items. [RelayCommand] private async Task ExecuteMailOperation(MailOperation mailOperation) { if (MailCollection.SelectedItemsCount == 0) return; await HandleMailOperation(mailOperation, MailCollection.SelectedItems); } 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() { var mails = MailCollection.SelectedItems; _notificationBuilder.CreateNotificationsAsync(mails.Select(a => a.MailCopy)); return; 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(CanExecute = nameof(CanLoadMoreItems))] private async Task LoadMoreItemsAsync() { if (IsInitializingFolder || IsOnlineSearchEnabled || FinishedLoading) return; Debug.WriteLine("Loading more..."); 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); if (items.Count == 0) { await ExecuteUIThread(() => { FinishedLoading = true; }); return; } var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false); await MailCollection.AddRangeAsync(viewModels, false); 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; } [RelayCommand] public void RemoveFirst() { var fi = MailCollection.GetFirst(); if (fi == null) return; Messenger.Send(new MailRemovedMessage(fi.MailCopy)); } /// /// Checks if a ThreadId exists in the current mail collection. /// /// The mail item to check ThreadId for. /// True if the ThreadId exists in the collection, false otherwise. private bool ThreadIdExistsInCollection(MailCopy mailItem) { return MailCollection.ContainsThreadId(mailItem.ThreadId); } 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 should only be inserted if their ThreadId exists in the collection. bool isFromDraftOrSentFolder = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent; if (isFromDraftOrSentFolder) { // Only add if the ThreadId exists in the collection (can be threaded with existing items) if (!ThreadIdExistsInCollection(addedMail)) return; } else { // Item does not belong to this folder. if (!ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return; // Item should be prevented from being added to the list due to filter. if (ShouldPreventItemAdd(addedMail)) return; } await listManipulationSemepahore.WaitAsync(); // AddAsync already handles UI threading internally, no need to wrap it await MailCollection.AddAsync(addedMail); await ExecuteUIThread(() => { NotifyItemFoundState(); }); } catch { } finally { listManipulationSemepahore.Release(); } } protected override async void OnMailUpdated(MailCopy updatedMail) { base.OnMailUpdated(updatedMail); Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); 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.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) { await ExecuteUIThread(() => { nextItem = MailCollection.GetNextItem(removedMail); }); } // RemoveAsync already handles UI threading internally await MailCollection.RemoveAsync(removedMail); if (nextItem != null) WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem.UniqueId, 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. // UnselectAllAsync already handles UI threading internally await MailCollection.UnselectAllAsync(); } 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(); // AddAsync already handles UI threading internally await MailCollection.AddAsync(draftMail); await ExecuteUIThread(() => { // New draft is created by user. Select the item. Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true)); NotifyItemFoundState(); }); } finally { listManipulationSemepahore.Release(); } } private async Task> PrepareMailViewModelsAsync(IEnumerable mailItems, CancellationToken cancellationToken = default) { // Run ViewModel creation on background thread to avoid blocking UI return await Task.Run(() => { var viewModels = new List(); foreach (var mailItem in mailItems) { cancellationToken.ThrowIfCancellationRequested(); viewModels.Add(new MailItemViewModel(mailItem)); } return viewModels; }, cancellationToken).ConfigureAwait(false); } [RelayCommand] private async Task PerformOnlineSearchAsync() { IsOnlineSearchButtonVisible = false; IsOnlineSearchEnabled = true; await InitializeFolderAsync(); } private async Task InitializeFolderAsync() { if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null) return; try { await MailCollection.ClearAsync(); 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 = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false); await MailCollection.AddRangeAsync(viewModels, clearIdCache: true); await ExecuteUIThread(() => { 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(); // Clear the loading message after completion IsBarOpen = false; }); } } #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) { // TODO: Remove this. WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(message.UniqueMailId, 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 async 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; // ClearAsync already handles UI threading internally await MailCollection.ClearAsync(); await ExecuteUIThread(() => { _mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning); }); } } protected override void OnDispatcherAssigned() { base.OnDispatcherAssigned(); MailCollection.CoreDispatcher = Dispatcher; } public void Receive(ThumbnailAdded message) { _ = MailCollection.UpdateThumbnailsForAddressAsync(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); 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); Messenger.Unregister>(this); } public void Receive(PropertyChangedMessage message) { // Handle IsSelected property changes from MailItemViewModel if (message.PropertyName == nameof(MailItemViewModel.IsSelected) && message.Sender is MailItemViewModel mailItemViewModel) { Messenger.Send(new SelectedItemsChangedMessage()); } else if (message.Sender is ThreadMailItemViewModel threadMailItemViewModel) { if (message.PropertyName == nameof(ThreadMailItemViewModel.IsSelected)) { // Thread selected. } else if (message.PropertyName == nameof(ThreadMailItemViewModel.IsThreadExpanded)) { // Thread expanded. } } } public async void Receive(SwipeActionRequested message) { if (message.MailItem == null) return; // Get mail copies based on the mail item type IEnumerable mailCopies; if (message.MailItem is MailItemViewModel singleItem) { mailCopies = new[] { singleItem.MailCopy }; } else if (message.MailItem is ThreadMailItemViewModel threadItem) { mailCopies = threadItem.ThreadEmails.Select(e => e.MailCopy); } else { return; // Unknown mail item type } var package = new MailOperationPreperationRequest(message.Operation, mailCopies); await ExecuteMailOperationAsync(package); } }