Files
Wino-Mail/Wino.Mail.ViewModels/MailListPageViewModel.cs
T

1508 lines
57 KiB
C#
Raw Normal View History

2024-04-18 01:44:37 +02:00
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;
2025-10-27 22:52:26 +01:00
using CommunityToolkit.Mvvm.Messaging.Messages;
2024-04-18 01:44:37 +02:00
using MoreLinq;
using Nito.AsyncEx;
using Serilog;
using Wino.Core.Domain;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
2026-03-08 13:21:42 +01:00
using Wino.Core.Domain.Models;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Synchronization;
2025-10-04 23:10:07 +02:00
using Wino.Core.Services;
2024-04-18 01:44:37 +02:00
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;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
namespace Wino.Mail.ViewModels;
public partial class MailListPageViewModel : MailBaseViewModel,
IRecipient<MailItemNavigationRequested>,
IRecipient<ActiveMailFolderChangedEvent>,
IRecipient<AccountSynchronizationCompleted>,
IRecipient<NewMailSynchronizationRequested>,
IRecipient<AccountSynchronizerStateChanged>,
IRecipient<AccountCacheResetMessage>,
2025-10-27 22:52:26 +01:00
IRecipient<ThumbnailAdded>,
2025-10-27 23:22:55 +01:00
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<SwipeActionRequested>
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
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<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = [];
public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection();
2025-02-16 11:54:23 +01:00
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
public ObservableCollection<MailOperationMenuItem> 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; }
2025-10-03 21:17:41 +02:00
public INewThemeService ThemeService { get; }
2025-02-16 11:54:23 +01:00
private readonly IAccountService _accountService;
2025-02-22 00:22:00 +01:00
private readonly IMailDialogService _mailDialogService;
2025-02-16 11:54:23 +01:00
private readonly IMailService _mailService;
2026-03-08 13:21:42 +01:00
private readonly IMimeFileService _mimeFileService;
private readonly INotificationBuilder _notificationBuilder;
2025-02-16 11:54:23 +01:00
private readonly IFolderService _folderService;
private readonly IContextMenuItemService _contextMenuItemService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService;
2025-02-22 00:22:00 +01:00
private readonly IWinoLogger _winoLogger;
2025-02-16 11:54:23 +01:00
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),
new (Translator.FilteringOption_Files, FilterOptionType.Files)
];
private FolderPivotViewModel _selectedFolderPivot;
[ObservableProperty]
private bool isMultiSelectionModeEnabled;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedMessageText))]
[NotifyPropertyChangedFor(nameof(DraggingMessageText))]
public partial bool IsDragInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedMessageText))]
[NotifyPropertyChangedFor(nameof(DraggingMessageText))]
public partial int DraggingItemsCount { get; set; }
2025-02-16 11:54:23 +01:00
[ObservableProperty]
2025-02-22 00:22:00 +01:00
public partial string SearchQuery { get; set; }
2025-02-16 11:54:23 +01:00
[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))]
2025-10-31 00:51:27 +01:00
[NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))]
public partial bool IsInitializingFolder { get; set; }
2025-02-16 11:54:23 +01:00
[ObservableProperty]
2025-10-31 00:51:27 +01:00
[NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))]
public partial bool FinishedLoading { get; set; } = false;
public bool CanLoadMoreItems => !IsInitializingFolder && !IsOnlineSearchEnabled && !FinishedLoading;
[ObservableProperty]
public partial InfoBarMessageType BarSeverity { get; set; }
2025-02-16 11:54:23 +01:00
[ObservableProperty]
2025-10-31 00:51:27 +01:00
public partial string BarMessage { get; set; }
2025-02-16 11:54:23 +01:00
[ObservableProperty]
public partial double MailListLength { get; set; } = 420;
2025-02-16 11:54:23 +01:00
[ObservableProperty]
2025-10-31 00:51:27 +01:00
public partial double MaxMailListLength { get; set; } = 1200;
2025-02-16 11:54:23 +01:00
[ObservableProperty]
2025-10-31 00:51:27 +01:00
public partial string BarTitle { get; set; }
2025-02-16 11:54:23 +01:00
[ObservableProperty]
2025-10-31 00:51:27 +01:00
public partial bool IsBarOpen { get; set; }
2025-02-16 11:54:23 +01:00
/// <summary>
/// Current folder that is being represented from the menu.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronize))]
[NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))]
2025-10-31 00:51:27 +01:00
public partial IBaseFolderMenuItem ActiveFolder { get; set; }
2025-02-16 11:54:23 +01:00
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronize))]
2025-10-31 00:51:27 +01:00
public partial bool IsAccountSynchronizerInSynchronization { get; set; }
2025-02-16 11:54:23 +01:00
public MailListPageViewModel(IMailDialogService dialogService,
INavigationService navigationService,
IAccountService accountService,
2025-02-22 00:22:00 +01:00
IMailDialogService mailDialogService,
2025-02-16 11:54:23 +01:00
IMailService mailService,
2026-03-08 13:21:42 +01:00
IMimeFileService mimeFileService,
2025-02-16 11:54:23 +01:00
IStatePersistanceService statePersistenceService,
INotificationBuilder notificationBuilder,
2025-02-16 11:54:23 +01:00
IFolderService folderService,
IContextMenuItemService contextMenuItemService,
IWinoRequestDelegator winoRequestDelegator,
IKeyPressService keyPressService,
IPreferencesService preferencesService,
2025-10-03 21:17:41 +02:00
INewThemeService themeService,
2025-10-04 23:10:07 +02:00
IWinoLogger winoLogger)
2024-04-18 01:44:37 +02:00
{
2025-02-22 00:22:00 +01:00
_winoLogger = winoLogger;
2025-02-16 11:54:23 +01:00
_accountService = accountService;
2025-02-22 00:22:00 +01:00
_mailDialogService = mailDialogService;
2025-02-16 11:54:23 +01:00
_mailService = mailService;
2026-03-08 13:21:42 +01:00
_mimeFileService = mimeFileService;
2025-02-16 11:54:23 +01:00
_folderService = folderService;
_contextMenuItemService = contextMenuItemService;
_winoRequestDelegator = winoRequestDelegator;
_keyPressService = keyPressService;
PreferencesService = preferencesService;
ThemeService = themeService;
StatePersistenceService = statePersistenceService;
_notificationBuilder = notificationBuilder;
NavigationService = navigationService;
2025-02-16 11:54:23 +01:00
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;
}
2025-02-16 11:43:30 +01:00
2025-10-27 01:43:36 +01:00
public override async void OnNavigatedFrom(NavigationMode mode, object parameters)
{
base.OnNavigatedFrom(mode, parameters);
MailCollection.ItemSelectionChanged -= MailItemSelectionChanged;
2025-10-27 01:43:36 +01:00
await MailCollection.ClearAsync();
MailCollection.Cleanup();
}
private void MailItemSelectionChanged(object sender, EventArgs e)
{
if (MailCollection.HasSingleItemSelected)
{
var selectedItem = MailCollection.SelectedItems.ElementAtOrDefault(0);
ActiveMailItemChanged(selectedItem);
}
2025-10-27 22:52:26 +01:00
else if (MailCollection.SelectedItemsCount == 0)
{
ActiveMailItemChanged(null);
}
NotifyItemFoundState();
NotifyItemSelected();
SetupTopBarActions();
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private void SetupTopBarActions()
{
ActionItems.Clear();
var actions = GetAvailableMailActions(MailCollection.SelectedItems);
2025-02-16 11:54:23 +01:00
actions.ForEach(a => ActionItems.Add(a));
}
2024-08-31 13:25:55 +02:00
2025-02-16 11:54:23 +01:00
#region Properties
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
/// <summary>
/// Selected internal folder. This can be either folder's own name or Focused-Other.
/// </summary>
public FolderPivotViewModel SelectedFolderPivot
{
get => _selectedFolderPivot;
set
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
if (_selectedFolderPivot != null)
_selectedFolderPivot.SelectedItemCount = 0;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
SetProperty(ref _selectedFolderPivot, value);
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
/// <summary>
/// Selected sorting option.
/// </summary>
public SortingOption SelectedSortingOption
{
get => _selectedSortingOption;
set
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
if (SetProperty(ref _selectedSortingOption, value))
2024-04-18 01:44:37 +02:00
{
2025-10-18 11:45:10 +02:00
if (value != null && MailCollection != null)
{
MailCollection.GroupingType = value.Type == SortingOptionType.ReceiveDate ? EmailGroupingType.ByDate : EmailGroupingType.ByFromName;
}
2024-04-18 01:44:37 +02:00
}
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
public string SelectedMessageText => IsDragInProgress
? string.Format(Translator.MailsDragging, DraggingItemsCount)
: MailCollection.SelectedItemsCount > 0
? string.Format(Translator.MailsSelected, MailCollection.SelectedItemsCount)
: Translator.NoMailSelected;
public string DraggingMessageText => string.Format(Translator.MailsDragging, DraggingItemsCount);
2025-02-22 00:22:00 +01:00
2025-02-16 11:54:23 +01:00
/// <summary>
/// Indicates current state of the mail list. Doesn't matter it's loading or no.
/// </summary>
public bool IsEmpty => MailCollection.AllItemsCount == 0;
2025-02-16 11:54:23 +01:00
/// <summary>
/// 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.
/// </summary>
public bool IsProgressRing => IsInitializingFolder && IsEmpty;
2025-02-22 00:22:00 +01:00
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; }
2025-02-16 11:54:23 +01:00
2025-02-22 00:22:00 +01:00
[ObservableProperty]
2025-10-31 00:51:27 +01:00
[NotifyCanExecuteChangedFor(nameof(LoadMoreItemsCommand))]
2025-02-22 00:22:00 +01:00
public partial bool IsOnlineSearchEnabled { get; set; }
[ObservableProperty]
public partial bool AreSearchResultsOnline { get; set; }
2025-02-16 11:54:23 +01:00
#endregion
private async void ActiveMailItemChanged(MailItemViewModel selectedMailItemViewModel)
{
if (_activeMailItem == selectedMailItemViewModel) return;
2024-05-09 00:51:16 +02:00
2025-02-16 11:54:23 +01:00
_activeMailItem = selectedMailItemViewModel;
2024-05-09 00:51:16 +02:00
2025-02-16 11:54:23 +01:00
Messenger.Send(new ActiveMailItemChangedEvent(_activeMailItem));
2024-05-09 00:51:16 +02:00
2025-02-16 11:54:23 +01:00
if (_activeMailItem == null || _activeMailItem.IsRead) return;
2024-05-09 00:51:16 +02:00
2025-02-16 11:54:23 +01:00
// Automatically set mark as read or not based on preferences.
2024-05-09 00:51:16 +02:00
2025-02-16 11:54:23 +01:00
var markAsPreference = PreferencesService.MarkAsPreference;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (markAsPreference == MailMarkAsOption.WhenSelected)
{
var operation = MailOperation.MarkAsRead;
var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy);
2024-05-09 00:51:16 +02:00
if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
!gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
{
gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
}
2024-05-09 00:51:16 +02:00
await ExecuteMailOperationAsync(package);
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
// TODO: Start a timer then queue.
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public void NotifyItemSelected()
{
OnPropertyChanged(nameof(SelectedMessageText));
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
public void SetDragState(bool isDragInProgress, int draggingItemsCount = 0)
{
IsDragInProgress = isDragInProgress;
DraggingItemsCount = isDragInProgress ? Math.Max(1, draggingItemsCount) : 0;
}
2025-02-16 11:54:23 +01:00
private void NotifyItemFoundState()
{
OnPropertyChanged(nameof(IsEmpty));
OnPropertyChanged(nameof(IsFolderEmpty));
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message)
{
await ExecuteUIThread(() =>
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
BarSeverity = severity;
BarTitle = title;
BarMessage = message;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
IsBarOpen = true;
});
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
private async Task UpdateFolderPivotsAsync()
{
if (ActiveFolder == null) return;
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
PivotFolders.Clear();
SelectedFolderPivot = null;
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
if (IsInSearchMode)
2025-02-16 11:54:23 +01:00
{
2025-02-22 00:22:00 +01:00
var isFocused = SelectedFolderPivot?.IsFocused;
PivotFolders.Add(new FolderPivotViewModel(Translator.SearchPivotName, isFocused));
2024-04-18 01:44:37 +02:00
}
2025-02-22 00:22:00 +01:00
else
2025-02-16 11:43:30 +01:00
{
2025-02-22 00:22:00 +01:00
// Merged folders don't support focused feature.
2025-02-22 00:22:00 +01:00
if (ActiveFolder is IMergedAccountFolderMenuItem)
2025-02-16 11:54:23 +01:00
{
2025-02-22 00:22:00 +01:00
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
2025-02-16 11:54:23 +01:00
}
2025-02-22 00:22:00 +01:00
else if (ActiveFolder is IFolderMenuItem singleFolderMenuItem)
2025-02-16 11:54:23 +01:00
{
2025-02-22 00:22:00 +01:00
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));
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
}
2025-02-22 00:22:00 +01:00
2025-02-16 11:54:23 +01:00
// This will trigger refresh.
SelectedFolderPivot = PivotFolders.FirstOrDefault();
}
2025-02-16 11:54:23 +01:00
#region Commands
2025-02-16 11:54:23 +01:00
[RelayCommand]
public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request);
2025-02-16 11:54:23 +01:00
[RelayCommand]
private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem)
{
if (menuItem == null || MailCollection.SelectedItemsCount == 0) return;
await HandleMailOperation(menuItem.Operation, MailCollection.SelectedItems);
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
/// <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 ExecuteMailOperation(MailOperation mailOperation)
{
if (MailCollection.SelectedItemsCount == 0) return;
await HandleMailOperation(mailOperation, MailCollection.SelectedItems);
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable<MailItemViewModel> mailItems)
{
if (!mailItems.Any()) return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var package = new MailOperationPreperationRequest(mailOperation, mailItems.Select(a => a.MailCopy));
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await ExecuteMailOperationAsync(package);
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
/// <summary>
/// Sens a new message to synchronize current folder.
/// </summary>
[RelayCommand]
private void SyncFolder()
{
if (!CanSynchronize) return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Only synchronize listed folders.
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// 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.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
trackingSynchronizationId = Guid.NewGuid();
completedTrackingSynchronizationCount = 0;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach (var folder in ActiveFolder.HandlingFolders)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
var options = new MailSynchronizationOptions()
{
AccountId = folder.MailAccountId,
Type = MailSynchronizationType.CustomFolders,
SynchronizationFolderIds = [folder.Id],
GroupedSynchronizationTrackingId = trackingSynchronizationId
};
2024-05-08 23:59:50 +02:00
2025-10-12 16:23:33 +02:00
Messenger.Send(new NewMailSynchronizationRequested(options));
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2024-05-08 23:59:50 +02:00
2025-02-16 11:54:23 +01:00
[RelayCommand]
private async Task SelectedPivotChanged()
{
if (isChangingFolder) return;
2024-05-08 23:59:50 +02:00
2025-02-16 11:54:23 +01:00
await InitializeFolderAsync();
}
2024-05-08 23:59:50 +02:00
2025-02-16 11:54:23 +01:00
[RelayCommand]
private async Task SelectedSortingChanged(SortingOption option)
{
SelectedSortingOption = option;
2024-05-08 23:59:50 +02:00
2025-02-16 11:54:23 +01:00
if (isChangingFolder) return;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
await InitializeFolderAsync();
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
[RelayCommand]
private async Task SelectedFilterChanged(FilterOption option)
{
SelectedFilterOption = option;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (isChangingFolder) return;
await InitializeFolderAsync();
}
[RelayCommand]
public async Task PerformSearchAsync()
{
2025-02-22 00:22:00 +01:00
IsOnlineSearchEnabled = false;
AreSearchResultsOnline = false;
2026-02-12 18:57:55 +01:00
HasNoOnlineSearchResult = false;
OnPropertyChanged(nameof(HasNoOnlineSearchResult));
2025-02-22 00:22:00 +01:00
IsInSearchMode = !string.IsNullOrEmpty(SearchQuery);
2025-02-22 00:22:00 +01:00
if (IsInSearchMode)
{
2025-02-22 00:22:00 +01:00
IsOnlineSearchButtonVisible = false;
}
2025-02-22 00:22:00 +01:00
await UpdateFolderPivotsAsync();
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
[RelayCommand]
private async Task EnableFolderSynchronizationAsync()
{
if (ActiveFolder == null) return;
2025-02-16 11:54:23 +01:00
foreach (var folder in ActiveFolder.HandlingFolders)
{
await _folderService.ChangeFolderSynchronizationStateAsync(folder.Id, true);
}
2025-02-16 11:54:23 +01:00
}
2025-10-31 00:51:27 +01:00
[RelayCommand(CanExecute = nameof(CanLoadMoreItems))]
2025-02-16 11:54:23 +01:00
private async Task LoadMoreItemsAsync()
{
2025-10-31 00:51:27 +01:00
if (IsInitializingFolder || IsOnlineSearchEnabled || FinishedLoading) return;
2025-10-30 17:15:05 +01:00
Debug.WriteLine("Loading more...");
2025-10-18 11:45:10 +02:00
await ExecuteUIThread(() => { IsInitializingFolder = true; });
2024-05-09 00:51:16 +02:00
2025-10-18 11:45:10 +02:00
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
SelectedFilterOption.Type,
SelectedSortingOption.Type,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
IsInSearchMode ? SearchQuery : string.Empty,
MailCollection.MailCopyIdHashSet);
2024-04-18 01:44:37 +02:00
2025-10-18 11:45:10 +02:00
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
2024-04-18 01:44:37 +02:00
2025-10-31 00:51:27 +01:00
if (items.Count == 0)
{
await ExecuteUIThread(() => { FinishedLoading = true; });
return;
}
2025-10-31 01:41:51 +01:00
var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false);
var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync().ConfigureAwait(false);
ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
2024-04-18 01:44:37 +02:00
await MailCollection.AddRangeAsync(viewModels, false);
2025-10-18 11:45:10 +02:00
await ExecuteUIThread(() => { IsInitializingFolder = false; });
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
#endregion
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package);
2024-04-18 01:44:37 +02:00
2026-03-08 13:21:42 +01:00
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Mail)
return;
var targetItems = GetShortcutTargetItems().ToList();
switch (args.Action)
{
case KeyboardShortcutAction.ToggleReadUnread:
if (!targetItems.Any()) return;
await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.MarkAsRead, targetItems.Select(x => x.MailCopy), true));
args.Handled = true;
break;
case KeyboardShortcutAction.ToggleFlag:
if (!targetItems.Any()) return;
await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.SetFlag, targetItems.Select(x => x.MailCopy), true));
args.Handled = true;
break;
case KeyboardShortcutAction.ToggleArchive:
if (!targetItems.Any()) return;
await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.Archive, targetItems.Select(x => x.MailCopy), true));
args.Handled = true;
break;
case KeyboardShortcutAction.Delete:
if (!targetItems.Any()) return;
await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.SoftDelete, targetItems.Select(x => x.MailCopy)));
args.Handled = true;
break;
case KeyboardShortcutAction.Move:
if (!targetItems.Any()) return;
await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.Move, targetItems.Select(x => x.MailCopy)));
args.Handled = true;
break;
case KeyboardShortcutAction.Reply:
await CreateReplyDraftAsync(DraftCreationReason.Reply);
args.Handled = true;
break;
case KeyboardShortcutAction.ReplyAll:
await CreateReplyDraftAsync(DraftCreationReason.ReplyAll);
args.Handled = true;
break;
}
}
private IEnumerable<MailItemViewModel> GetShortcutTargetItems()
{
if (MailCollection.SelectedItemsCount > 0)
return MailCollection.SelectedItems.OfType<MailItemViewModel>();
if (_activeMailItem != null)
return [_activeMailItem];
return [];
}
private async Task CreateReplyDraftAsync(DraftCreationReason reason)
{
var targetMail = GetShortcutTargetItems().FirstOrDefault();
if (targetMail?.MailCopy == null || targetMail.MailCopy.FileId == Guid.Empty)
return;
var mimeInformation = await _mimeFileService.GetMimeMessageInformationAsync(targetMail.MailCopy.FileId, targetMail.MailCopy.AssignedAccount.Id);
if (mimeInformation?.MimeMessage == null)
return;
var draftOptions = new DraftCreationOptions
{
Reason = reason,
ReferencedMessage = new ReferencedMessage
{
MimeMessage = mimeInformation.MimeMessage,
MailCopy = targetMail.MailCopy
}
};
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(targetMail.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(targetMail.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, targetMail.MailCopy);
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
}
2025-10-03 15:46:38 +02:00
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems)
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
2024-04-18 01:44:37 +02:00
2025-10-03 15:46:38 +02:00
private bool ShouldPreventItemAdd(MailCopy mailItem)
2025-02-16 11:54:23 +01:00
{
bool condition = mailItem.IsRead
&& SelectedFilterOption.Type == FilterOptionType.Unread
|| !mailItem.IsFlagged
&& SelectedFilterOption.Type == FilterOptionType.Flagged;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return condition;
}
2024-04-18 01:44:37 +02:00
2026-02-12 18:57:55 +01:00
private static bool IsDraftOrSentFolder(MailCopy mailItem)
=> mailItem?.AssignedFolder?.SpecialFolderType is SpecialFolderType.Draft or SpecialFolderType.Sent;
2026-02-14 12:52:17 +01:00
private bool IsActiveDraftFolder()
=> ActiveFolder?.SpecialFolderType == SpecialFolderType.Draft;
private bool BelongsToActiveFolder(MailCopy mailItem)
=> mailItem?.AssignedFolder != null && ActiveFolder?.HandlingFolders?.Any(a => a.Id == mailItem.AssignedFolder.Id) == true;
private bool ShouldIncludeByThread(MailCopy mailItem)
=> PreferencesService.IsThreadingEnabled
&& !string.IsNullOrEmpty(mailItem?.ThreadId)
&& ThreadIdExistsInCollection(mailItem);
private bool ShouldIncludeAddedMailInCurrentList(MailCopy addedMail)
{
if (addedMail == null || ActiveFolder == null || addedMail.AssignedFolder == null)
return false;
// 1) If threading is enabled and we already have the same conversation in view, include it.
if (ShouldIncludeByThread(addedMail))
return true;
// 2) Include items that belong to the active folder.
if (BelongsToActiveFolder(addedMail))
return true;
// 3) Draft-specific visibility: include drafts while viewing Drafts.
if (addedMail.IsDraft && IsActiveDraftFolder())
return true;
return false;
}
2026-02-12 18:57:55 +01:00
private bool IsMailMatchingLocalSearch(MailCopy mailItem)
{
if (!IsInSearchMode) return true;
if (string.IsNullOrWhiteSpace(SearchQuery)) return true;
var query = SearchQuery.Trim();
return (!string.IsNullOrEmpty(mailItem.Subject) && mailItem.Subject.Contains(query, StringComparison.OrdinalIgnoreCase))
|| (!string.IsNullOrEmpty(mailItem.PreviewText) && mailItem.PreviewText.Contains(query, StringComparison.OrdinalIgnoreCase))
|| (!string.IsNullOrEmpty(mailItem.FromName) && mailItem.FromName.Contains(query, StringComparison.OrdinalIgnoreCase))
|| (!string.IsNullOrEmpty(mailItem.FromAddress) && mailItem.FromAddress.Contains(query, StringComparison.OrdinalIgnoreCase));
}
private bool ShouldRemoveUpdatedMailFromCurrentList(MailCopy updatedMail)
{
2026-02-14 12:52:17 +01:00
// Update flow already checks if this item is currently listed.
// Keep the item in the list and update in-place.
_ = updatedMail;
return false;
2026-02-12 18:57:55 +01:00
}
[RelayCommand]
public void RemoveFirst()
{
var fi = MailCollection.GetFirst();
if (fi == null) return;
Messenger.Send(new MailRemovedMessage(fi.MailCopy));
}
/// <summary>
/// Checks if a ThreadId exists in the current mail collection.
/// </summary>
/// <param name="mailItem">The mail item to check ThreadId for.</param>
/// <returns>True if the ThreadId exists in the collection, false otherwise.</returns>
private bool ThreadIdExistsInCollection(MailCopy mailItem)
{
return MailCollection.ContainsThreadId(mailItem.ThreadId);
}
2025-02-16 11:54:23 +01:00
protected override async void OnMailAdded(MailCopy addedMail)
{
base.OnMailAdded(addedMail);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (addedMail.AssignedAccount == null || addedMail.AssignedFolder == null) return;
2025-02-16 11:54:23 +01:00
try
{
if (ActiveFolder == null) return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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;
2024-04-18 01:44:37 +02:00
2026-02-14 12:52:17 +01:00
// Fix for draft duplication: When a draft is created for reply/forward, it's first added as local draft.
// Then the server sync fetches it back. We should skip adding remote drafts if a local draft already exists
// with the same ThreadId. The mapping system (DraftMapped) will handle updating the existing local draft.
if (addedMail.IsDraft && !addedMail.IsLocalDraft && !string.IsNullOrEmpty(addedMail.ThreadId))
{
2026-02-14 12:52:17 +01:00
// Check if collection already has a local draft with the same ThreadId in the same folder
bool hasLocalDraftInSameThread = false;
foreach (var group in MailCollection.MailItems)
{
2026-02-14 12:52:17 +01:00
foreach (var item in group)
{
2026-02-14 12:52:17 +01:00
if (item is MailItemViewModel mailItem)
{
2026-02-14 12:52:17 +01:00
if (mailItem.IsDraft &&
mailItem.MailCopy.IsLocalDraft &&
mailItem.MailCopy.ThreadId == addedMail.ThreadId &&
mailItem.MailCopy.FolderId == addedMail.FolderId)
{
2026-02-14 12:52:17 +01:00
hasLocalDraftInSameThread = true;
break;
}
2026-02-14 12:52:17 +01:00
}
else if (item is ThreadMailItemViewModel threadItem)
{
foreach (var threadEmail in threadItem.ThreadEmails)
{
2026-02-14 12:52:17 +01:00
if (threadEmail.IsDraft &&
threadEmail.MailCopy.IsLocalDraft &&
threadEmail.MailCopy.ThreadId == addedMail.ThreadId &&
threadEmail.MailCopy.FolderId == addedMail.FolderId)
{
2026-02-14 12:52:17 +01:00
hasLocalDraftInSameThread = true;
break;
}
}
2026-02-14 12:52:17 +01:00
if (hasLocalDraftInSameThread) break;
}
}
2026-02-14 12:52:17 +01:00
if (hasLocalDraftInSameThread) break;
}
2026-02-14 12:52:17 +01:00
if (hasLocalDraftInSameThread)
{
// Local draft exists in the same thread - skip adding remote duplicate
// The mapping system will update the local draft with remote IDs when DraftMapped message is received
return;
}
}
2024-04-18 01:44:37 +02:00
2026-02-14 12:52:17 +01:00
if (!ShouldIncludeAddedMailInCurrentList(addedMail)) return;
if (ShouldPreventItemAdd(addedMail)) return;
2024-08-31 13:25:55 +02:00
2026-02-12 18:57:55 +01:00
if (SelectedFolderPivot?.IsFocused is bool isFocused && addedMail.IsFocused != isFocused)
{
return;
}
if (IsInSearchMode)
{
// Online search results are loaded from a dedicated query snapshot.
// Ignore live additions while that snapshot is active.
if (IsOnlineSearchEnabled || AreSearchResultsOnline) return;
if (!IsMailMatchingLocalSearch(addedMail)) return;
}
2025-02-16 11:54:23 +01:00
await listManipulationSemepahore.WaitAsync();
2024-04-18 01:44:37 +02:00
2025-10-31 00:51:27 +01:00
// AddAsync already handles UI threading internally, no need to wrap it
await MailCollection.AddAsync(addedMail);
await ExecuteUIThread(() =>
{
NotifyItemFoundState();
});
2025-02-16 11:54:23 +01:00
}
catch { }
finally
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
listManipulationSemepahore.Release();
}
}
2024-04-18 01:44:37 +02:00
protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties)
2025-02-16 11:54:23 +01:00
{
base.OnMailUpdated(updatedMail, source, changedProperties);
try
{
await listManipulationSemepahore.WaitAsync();
2026-02-12 18:57:55 +01:00
bool isItemListed = MailCollection.ContainsMailUniqueId(updatedMail.UniqueId);
if (!isItemListed) return;
if (ShouldRemoveUpdatedMailFromCurrentList(updatedMail))
{
await MailCollection.RemoveAsync(updatedMail);
await ExecuteUIThread(() => { NotifyItemFoundState(); });
return;
}
await MailCollection.UpdateMailCopy(updatedMail, source, changedProperties);
}
finally
{
listManipulationSemepahore.Release();
}
2024-04-18 01:44:37 +02:00
// await ExecuteUIThread(() => { SetupTopBarActions(); });
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
protected override async void OnMailRemoved(MailCopy removedMail)
{
base.OnMailRemoved(removedMail);
2024-04-18 01:44:37 +02:00
if (removedMail.AssignedAccount == null) return;
try
{
await listManipulationSemepahore.WaitAsync();
2024-04-18 01:44:37 +02:00
// Remove only if this specific mail copy currently exists in this list.
// Using AssignedFolder-based checks is unreliable for move flows because the
// same MailCopy instance can be updated before this message is handled.
bool removedItemExistsInCurrentList = MailCollection.ContainsMailUniqueId(removedMail.UniqueId);
2024-04-18 01:44:37 +02:00
bool isDeletedByGmailUnreadFolderAction = ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId);
2024-04-18 01:44:37 +02:00
if (removedItemExistsInCurrentList && !isDeletedByGmailUnreadFolderAction)
{
bool isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
2024-04-18 01:44:37 +02:00
// Automatically select the next item in the list if the setting is enabled.
MailItemViewModel nextItem = null;
2025-02-16 11:35:43 +01:00
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
{
await ExecuteUIThread(() =>
{
nextItem = MailCollection.GetNextItem(removedMail);
});
}
2025-02-16 11:54:23 +01:00
// 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)
2025-02-23 17:05:46 +01:00
{
// There are no next item to select, but we removed the last item which was selected.
// Clearing selected item will dispose rendering page.
2025-02-16 11:43:30 +01:00
// UnselectAllAsync already handles UI threading internally
await MailCollection.UnselectAllAsync();
}
2024-04-18 01:44:37 +02:00
await ExecuteUIThread(() => { NotifyItemFoundState(); });
}
else if (isDeletedByGmailUnreadFolderAction)
2025-02-16 11:43:30 +01:00
{
// Remove the entry from the set so we can listen to actual deletes next time.
gmailUnreadFolderMarkedAsReadUniqueIds.Remove(removedMail.UniqueId);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
finally
2025-02-16 11:35:43 +01:00
{
listManipulationSemepahore.Release();
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2026-02-08 22:20:38 +01:00
protected override async void OnFolderDeleted(MailItemFolder folder)
{
base.OnFolderDeleted(folder);
if (ActiveFolder == null) return;
bool isActiveFolder = ActiveFolder.HandlingFolders.Any(a => a.Id == folder.Id);
if (isActiveFolder)
{
await MailCollection.ClearAsync();
}
}
2025-02-16 11:54:23 +01:00
protected override async void OnDraftCreated(MailCopy draftMail, MailAccount account)
{
base.OnDraftCreated(draftMail, account);
try
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
// 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();
2024-04-18 01:44:37 +02:00
2025-10-31 00:51:27 +01:00
// AddAsync already handles UI threading internally
await MailCollection.AddAsync(draftMail);
2025-10-03 15:46:38 +02:00
2025-10-31 00:51:27 +01:00
await ExecuteUIThread(() =>
{
2025-02-16 11:54:23 +01:00
// New draft is created by user. Select the item.
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
NotifyItemFoundState();
});
}
finally
{
listManipulationSemepahore.Release();
}
}
2024-04-18 01:44:37 +02:00
protected override void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId)
{
base.OnDraftMapped(localDraftCopyId, remoteDraftCopyId);
// When a draft is mapped from local to remote, the database has been updated
// but the UI collection still references the MailCopy object with old IDs.
// The MailCollection.AddAsync method checks UniqueId (which doesn't change during mapping)
// so if mapping worked correctly, no duplicate should appear.
// This method is here for future enhancements if additional UI updates are needed.
}
2025-10-31 01:41:51 +01:00
private async Task<List<MailItemViewModel>> PrepareMailViewModelsAsync(IEnumerable<MailCopy> mailItems, CancellationToken cancellationToken = default)
2025-02-16 11:54:23 +01:00
{
2025-10-31 01:41:51 +01:00
// Run ViewModel creation on background thread to avoid blocking UI
return await Task.Run(() =>
{
var viewModels = new List<MailItemViewModel>();
foreach (var mailItem in mailItems)
{
cancellationToken.ThrowIfCancellationRequested();
viewModels.Add(new MailItemViewModel(mailItem));
}
return viewModels;
}, cancellationToken).ConfigureAwait(false);
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
private async Task<HashSet<Guid>> GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(CancellationToken cancellationToken = default)
{
var pendingOperationUniqueIds = new HashSet<Guid>();
var accountIds = ActiveFolder?.HandlingFolders?
.Select(folder => folder.MailAccountId)
.Where(accountId => accountId != Guid.Empty)
.Distinct()
.ToList();
if (accountIds == null || accountIds.Count == 0)
return pendingOperationUniqueIds;
foreach (var accountId in accountIds)
{
cancellationToken.ThrowIfCancellationRequested();
var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
if (synchronizer == null)
continue;
foreach (var uniqueId in synchronizer.GetPendingOperationUniqueIds())
{
pendingOperationUniqueIds.Add(uniqueId);
}
}
return pendingOperationUniqueIds;
}
private static void ApplyPendingOperationBusyStates(IEnumerable<MailItemViewModel> viewModels, HashSet<Guid> pendingOperationUniqueIds)
{
if (viewModels == null || pendingOperationUniqueIds == null || pendingOperationUniqueIds.Count == 0)
return;
foreach (var viewModel in viewModels)
{
viewModel.IsBusy = pendingOperationUniqueIds.Contains(viewModel.MailCopy.UniqueId);
}
}
2025-02-22 00:22:00 +01:00
[RelayCommand]
private async Task PerformOnlineSearchAsync()
{
IsOnlineSearchButtonVisible = false;
IsOnlineSearchEnabled = true;
await InitializeFolderAsync();
}
2026-02-12 18:57:55 +01:00
private async Task<List<MailCopy>> PerformSynchronizerOnlineSearchAsync(string queryText,
IEnumerable<IMailItemFolder> handlingFolders,
CancellationToken cancellationToken)
{
if (handlingFolders == null) return [];
var foldersByAccount = handlingFolders
.GroupBy(a => a.MailAccountId)
.ToList();
if (foldersByAccount.Count == 0) return [];
var searchTasks = foldersByAccount.Select(async groupedFolders =>
{
var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(groupedFolders.Key).ConfigureAwait(false);
if (synchronizer == null) return new List<MailCopy>();
var accountResults = await synchronizer.OnlineSearchAsync(queryText, groupedFolders.ToList(), cancellationToken).ConfigureAwait(false);
return accountResults ?? new List<MailCopy>();
});
var allResults = await Task.WhenAll(searchTasks).ConfigureAwait(false);
return allResults
.SelectMany(a => a)
.GroupBy(a => a.UniqueId)
.Select(a => a.First())
.ToList();
}
2025-02-16 11:54:23 +01:00
private async Task InitializeFolderAsync()
{
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
return;
2025-02-16 11:54:23 +01:00
try
{
2025-10-26 23:35:09 +01:00
await MailCollection.ClearAsync();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (ActiveFolder == null)
return;
2024-04-18 01:44:37 +02:00
2025-10-31 19:53:48 +01:00
await ExecuteUIThread(() => { IsInitializingFolder = true; });
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Folder is changed during initialization.
// Just cancel the existing one and wait for new initialization.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
listManipulationCancellationTokenSource.Cancel();
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
listManipulationCancellationTokenSource = new CancellationTokenSource();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var cancellationToken = listManipulationCancellationTokenSource.Token;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await listManipulationSemepahore.WaitAsync(cancellationToken);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Here items are sorted and filtered.
2024-04-18 01:44:37 +02:00
2025-10-03 15:46:38 +02:00
List<MailCopy> items = null;
2026-02-12 18:57:55 +01:00
List<MailCopy> onlineSearchItems = null;
2025-02-22 00:22:00 +01:00
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
bool isDoingOnlineSearch = false;
if (isDoingSearch)
{
isDoingOnlineSearch = PreferencesService.DefaultSearchMode == SearchMode.Online || IsOnlineSearchEnabled;
// Perform online search.
if (isDoingOnlineSearch)
{
2026-02-12 18:57:55 +01:00
try
{
onlineSearchItems = await PerformSynchronizerOnlineSearchAsync(SearchQuery, ActiveFolder.HandlingFolders, cancellationToken).ConfigureAwait(false);
await ExecuteUIThread(() => { AreSearchResultsOnline = true; });
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to perform online search.");
isDoingOnlineSearch = false;
onlineSearchItems = null;
await ExecuteUIThread(() =>
{
IsOnlineSearchEnabled = false;
AreSearchResultsOnline = false;
var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, ex.Message);
_mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
});
}
2025-02-22 00:22:00 +01:00
}
}
2025-02-16 11:54:23 +01:00
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
SelectedFilterOption.Type,
SelectedSortingOption.Type,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
2026-02-12 18:57:55 +01:00
isDoingOnlineSearch ? string.Empty : SearchQuery,
MailCollection.MailCopyIdHashSet,
onlineSearchItems);
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
2025-02-16 11:54:23 +01:00
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
// Here they are already threaded if needed.
// We don't need to insert them one by one.
// Just create VMs and do bulk insert.
2024-04-18 01:44:37 +02:00
2025-10-31 01:41:51 +01:00
var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false);
var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(cancellationToken).ConfigureAwait(false);
ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
2025-10-31 01:41:51 +01:00
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
2025-02-22 00:22:00 +01:00
await ExecuteUIThread(() =>
{
2026-02-12 18:57:55 +01:00
HasNoOnlineSearchResult = isDoingOnlineSearch && items.Count == 0;
OnPropertyChanged(nameof(HasNoOnlineSearchResult));
2025-02-22 00:22:00 +01:00
if (isDoingSearch && !isDoingOnlineSearch)
{
IsOnlineSearchButtonVisible = true;
}
});
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
catch (OperationCanceledException)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
Debug.WriteLine("Initialization of mails canceled.");
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
catch (Exception ex)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
Debugger.Break();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (IsInSearchMode)
Log.Error(ex, "Failed to perform search.");
else
Log.Error(ex, "Failed to refresh listed mails.");
}
finally
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
listManipulationSemepahore.Release();
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() =>
{
IsInitializingFolder = false;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
OnPropertyChanged(nameof(CanSynchronize));
NotifyItemFoundState();
2025-10-31 19:53:48 +01:00
2025-10-31 01:41:51 +01:00
// Clear the loading message after completion
IsBarOpen = false;
2025-02-16 11:54:23 +01:00
});
}
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
#region Receivers
2025-02-16 11:54:23 +01:00
async void IRecipient<ActiveMailFolderChangedEvent>.Receive(ActiveMailFolderChangedEvent message)
{
NotifyItemSelected();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
isChangingFolder = true;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
ActiveFolder = message.BaseFolderMenuItem;
gmailUnreadFolderMarkedAsReadUniqueIds.Clear();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
trackingSynchronizationId = null;
completedTrackingSynchronizationCount = 0;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Notify change for archive-unarchive app bar button.
OnPropertyChanged(nameof(IsArchiveSpecialFolder));
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
IsInSearchMode = false;
IsOnlineSearchButtonVisible = false;
AreSearchResultsOnline = false;
2026-02-12 18:57:55 +01:00
HasNoOnlineSearchResult = false;
OnPropertyChanged(nameof(HasNoOnlineSearchResult));
2025-02-22 00:22:00 +01:00
2025-02-16 11:54:23 +01:00
// Prepare Focused - Other or folder name tabs.
await UpdateFolderPivotsAsync();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Reset filters and sorting options.
ResetFilters();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await InitializeFolderAsync();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// TODO: This should be done in a better way.
while (IsInitializingFolder)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await Task.Delay(100);
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Check whether the account synchronizer that this folder belongs to is already in synchronization.
await CheckIfAccountIsSynchronizingAsync();
2025-02-16 11:54:23 +01:00
// Let awaiters know about the completion of mail init.
message.FolderInitLoadAwaitTask?.TrySetResult(true);
2025-02-16 11:54:23 +01:00
await Task.Yield();
2025-02-16 11:54:23 +01:00
isChangingFolder = false;
2025-02-16 11:54:23 +01:00
void ResetFilters()
{
// Expected that FilterOptions and SortingOptions have default value in 0 index.
SelectedFilterOption = FilterOptions[0];
SelectedSortingOption = SortingOptions[0];
SearchQuery = string.Empty;
IsInSearchMode = false;
2025-02-22 00:22:00 +01:00
IsOnlineSearchEnabled = false;
2026-02-12 18:57:55 +01:00
HasNoOnlineSearchResult = false;
2025-02-16 11:54:23 +01:00
}
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public void Receive(AccountSynchronizationCompleted message)
{
if (ActiveFolder == null) return;
2025-02-16 11:54:23 +01:00
bool isLinkedInboxSyncResult = message.SynchronizationTrackingId == trackingSynchronizationId;
2025-02-16 11:54:23 +01:00
if (isLinkedInboxSyncResult)
{
2025-02-16 11:54:23 +01:00
var isCompletedAccountListed = ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId);
2025-02-16 11:54:23 +01:00
if (isCompletedAccountListed) completedTrackingSynchronizationCount++;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// Group sync is started but not all folders are synchronized yet. Don't report progress.
if (completedTrackingSynchronizationCount < ActiveFolder.HandlingFolders.Count()) return;
}
2025-02-16 11:54:23 +01:00
bool isReportingActiveAccountResult = ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId);
2025-02-16 11:54:23 +01:00
if (!isReportingActiveAccountResult) return;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// 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.PartiallyCompleted:
UpdateBarMessage(InfoBarMessageType.Warning, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed);
break;
2025-02-16 11:54:23 +01:00
case SynchronizationCompletedState.Failed:
UpdateBarMessage(InfoBarMessageType.Error, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed);
break;
default:
break;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
void IRecipient<MailItemNavigationRequested>.Receive(MailItemNavigationRequested message)
{
// TODO: Remove this.
2024-04-18 01:44:37 +02:00
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(message.UniqueMailId, message.ScrollToItem));
2025-02-16 11:54:23 +01:00
}
#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(() =>
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
ActiveFolder.UpdateFolder(mailItemFolder);
OnPropertyChanged(nameof(CanSynchronize));
OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
});
SyncFolderCommand?.Execute(null);
}
public async void Receive(AccountSynchronizerStateChanged message)
=> await CheckIfAccountIsSynchronizingAsync();
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
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.
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (ActiveFolder != null)
{
var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId);
foreach (var accountId in accountIds)
2024-04-18 01:44:37 +02:00
{
2025-10-04 23:10:07 +02:00
if (SynchronizationManager.Instance.IsAccountSynchronizing(accountId))
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
isAnyAccountSynchronizing = true;
break;
2024-04-18 01:44:37 +02:00
}
}
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() => { IsAccountSynchronizerInSynchronization = isAnyAccountSynchronizing; });
2024-04-18 01:44:37 +02:00
}
2025-10-31 00:51:27 +01:00
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;
2025-10-31 00:51:27 +01:00
// ClearAsync already handles UI threading internally
await MailCollection.ClearAsync();
2025-10-31 00:51:27 +01:00
await ExecuteUIThread(() =>
{
_mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning);
});
}
}
protected override void OnDispatcherAssigned()
{
base.OnDispatcherAssigned();
MailCollection.CoreDispatcher = Dispatcher;
}
2025-10-03 15:46:38 +02:00
public void Receive(ThumbnailAdded message)
{
_ = MailCollection.UpdateThumbnailsForAddressAsync(message.Email);
2025-10-03 15:46:38 +02:00
}
protected override void RegisterRecipients()
{
base.RegisterRecipients();
Messenger.Register<MailItemNavigationRequested>(this);
Messenger.Register<ActiveMailFolderChangedEvent>(this);
Messenger.Register<AccountSynchronizationCompleted>(this);
Messenger.Register<NewMailSynchronizationRequested>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this);
Messenger.Register<AccountCacheResetMessage>(this);
Messenger.Register<ThumbnailAdded>(this);
2025-10-27 22:52:26 +01:00
Messenger.Register<PropertyChangedMessage<bool>>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
Messenger.Unregister<MailItemNavigationRequested>(this);
Messenger.Unregister<ActiveMailFolderChangedEvent>(this);
Messenger.Unregister<AccountSynchronizationCompleted>(this);
Messenger.Unregister<NewMailSynchronizationRequested>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
Messenger.Unregister<AccountCacheResetMessage>(this);
Messenger.Unregister<ThumbnailAdded>(this);
2025-10-27 22:52:26 +01:00
Messenger.Unregister<PropertyChangedMessage<bool>>(this);
}
public void Receive(PropertyChangedMessage<bool> 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.
}
}
}
2025-10-27 23:22:55 +01:00
public async void Receive(SwipeActionRequested message)
{
if (message.MailItem == null) return;
// Get mail copies based on the mail item type
IEnumerable<MailCopy> mailCopies;
2025-10-27 23:22:55 +01:00
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);
}
2024-04-18 01:44:37 +02:00
}