2024-04-18 01:44:37 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2026-04-21 23:17:08 +02:00
|
|
|
using System.Collections.Concurrent;
|
2024-04-18 01:44:37 +02:00
|
|
|
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;
|
2026-03-08 13:21:42 +01:00
|
|
|
using Wino.Core.Domain.Models;
|
2024-07-09 01:05:16 +02:00
|
|
|
using Wino.Core.Domain.Models.Folders;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain.Models.MailItem;
|
|
|
|
|
using Wino.Core.Domain.Models.Menus;
|
2025-10-21 22:08:56 +02:00
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain.Models.Reader;
|
2026-04-20 02:18:23 +02:00
|
|
|
using Wino.Core.Domain.Models.Synchronization;
|
2026-04-15 01:18:07 +02:00
|
|
|
using Wino.Core.Requests.Mail;
|
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;
|
2024-08-05 00:36:26 +02:00
|
|
|
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>,
|
2025-02-22 23:09:53 +01:00
|
|
|
IRecipient<AccountSynchronizerStateChanged>,
|
2025-06-21 01:40:25 +02:00
|
|
|
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 = [];
|
|
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection();
|
2025-02-16 11:54:23 +01:00
|
|
|
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
|
2026-04-03 19:50:52 +02:00
|
|
|
public ObservableCollection<IMenuOperation> ActionItems { get; set; } = [];
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
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;
|
2025-11-12 15:44:43 +01:00
|
|
|
private readonly INotificationBuilder _notificationBuilder;
|
2025-02-16 11:54:23 +01:00
|
|
|
private readonly IFolderService _folderService;
|
|
|
|
|
private readonly IContextMenuItemService _contextMenuItemService;
|
2026-04-20 02:18:23 +02:00
|
|
|
private readonly ILogger _logger = Log.ForContext<MailListPageViewModel>();
|
2026-04-15 01:18:07 +02:00
|
|
|
private readonly IMailCategoryService _mailCategoryService;
|
2025-02-16 11:54:23 +01:00
|
|
|
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;
|
|
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
[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]
|
2025-10-26 14:53:22 +01:00
|
|
|
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))]
|
2026-04-15 01:18:07 +02:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsCategoryView))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsSyncButtonVisible))]
|
2026-04-18 15:34:14 +02:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsJunkFolder))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsEmptyFolderButtonVisible))]
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(EmptyFolderCommand))]
|
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))]
|
2026-04-18 15:34:14 +02:00
|
|
|
[NotifyCanExecuteChangedFor(nameof(EmptyFolderCommand))]
|
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,
|
2025-11-12 15:44:43 +01:00
|
|
|
INotificationBuilder notificationBuilder,
|
2025-02-16 11:54:23 +01:00
|
|
|
IFolderService folderService,
|
|
|
|
|
IContextMenuItemService contextMenuItemService,
|
2026-04-15 01:18:07 +02:00
|
|
|
IMailCategoryService mailCategoryService,
|
2025-02-16 11:54:23 +01:00
|
|
|
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;
|
2026-04-15 01:18:07 +02:00
|
|
|
_mailCategoryService = mailCategoryService;
|
2025-02-16 11:54:23 +01:00
|
|
|
_winoRequestDelegator = winoRequestDelegator;
|
|
|
|
|
_keyPressService = keyPressService;
|
|
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
PreferencesService = preferencesService;
|
|
|
|
|
ThemeService = themeService;
|
|
|
|
|
StatePersistenceService = statePersistenceService;
|
2025-11-12 15:44:43 +01:00
|
|
|
_notificationBuilder = notificationBuilder;
|
2025-10-26 14:53:22 +01:00
|
|
|
NavigationService = navigationService;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
SelectedFilterOption = FilterOptions[0];
|
|
|
|
|
SelectedSortingOption = SortingOptions[0];
|
2026-04-19 16:25:00 +02:00
|
|
|
MailCollection.ThreadItemFactory = threadId => new ThreadMailItemViewModel(threadId, PreferencesService.IsNewestThreadMailFirst);
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
MailListLength = statePersistenceService.MailListPaneLength;
|
2025-10-21 22:08:56 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2025-10-21 22:08:56 +02:00
|
|
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedTo(mode, parameters);
|
|
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
MailCollection.ItemSelectionChanged += MailItemSelectionChanged;
|
2025-10-21 22:08:56 +02:00
|
|
|
}
|
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)
|
2025-10-21 22:08:56 +02:00
|
|
|
{
|
|
|
|
|
base.OnNavigatedFrom(mode, parameters);
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
MailCollection.ItemSelectionChanged -= MailItemSelectionChanged;
|
2025-10-27 01:43:36 +01:00
|
|
|
|
|
|
|
|
await MailCollection.ClearAsync();
|
|
|
|
|
MailCollection.Cleanup();
|
2025-10-21 22:08:56 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
private void MailItemSelectionChanged(object sender, EventArgs e)
|
2025-10-21 22:08:56 +02:00
|
|
|
{
|
2025-10-26 14:53:22 +01:00
|
|
|
if (MailCollection.HasSingleItemSelected)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2025-10-26 14:53:22 +01:00
|
|
|
var selectedItem = MailCollection.SelectedItems.ElementAtOrDefault(0);
|
|
|
|
|
ActiveMailItemChanged(selectedItem);
|
|
|
|
|
}
|
2025-10-27 22:52:26 +01:00
|
|
|
else if (MailCollection.SelectedItemsCount == 0)
|
2025-10-26 14:53:22 +01:00
|
|
|
{
|
|
|
|
|
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();
|
2025-10-26 14:53:22 +01:00
|
|
|
|
|
|
|
|
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-02-16 11:54:23 +01:00
|
|
|
if (value != null && MailCollection != null)
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-10-18 11:45:10 +02:00
|
|
|
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
|
|
|
|
2026-04-15 01:18:07 +02:00
|
|
|
public bool CanSynchronize => !IsCategoryView && !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
|
2025-02-16 11:54:23 +01:00
|
|
|
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
|
|
|
|
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
2026-04-18 15:34:14 +02:00
|
|
|
public bool IsJunkFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Junk;
|
2026-04-15 01:18:07 +02:00
|
|
|
public bool IsCategoryView => ActiveFolder is IMailCategoryMenuItem or IMergedMailCategoryMenuItem;
|
|
|
|
|
public bool IsSyncButtonVisible => !IsCategoryView;
|
2026-04-18 15:34:14 +02:00
|
|
|
public bool IsEmptyFolderButtonVisible => IsJunkFolder;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
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>
|
2025-10-26 14:53:22 +01:00
|
|
|
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
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
|
|
|
|
|
!gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
|
2024-05-09 00:51:16 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
|
2024-05-09 00:51:16 +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
|
|
|
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));
|
|
|
|
|
|
2025-10-21 22:08:56 +02:00
|
|
|
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
public void SetDragState(bool isDragInProgress, int draggingItemsCount = 0)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-02-11 11:34:50 +01:00
|
|
|
IsDragInProgress = isDragInProgress;
|
|
|
|
|
DraggingItemsCount = isDragInProgress ? Math.Max(1, draggingItemsCount) : 0;
|
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 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
|
|
|
{
|
2026-04-15 01:18:07 +02:00
|
|
|
if (IsCategoryView)
|
|
|
|
|
{
|
|
|
|
|
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
|
|
|
|
|
}
|
2025-02-22 00:22:00 +01:00
|
|
|
// Merged folders don't support focused feature.
|
2026-04-15 01:18:07 +02:00
|
|
|
else 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
|
|
|
}
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-22 00:22:00 +01:00
|
|
|
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// This will trigger refresh.
|
|
|
|
|
SelectedFolderPivot = PivotFolders.FirstOrDefault();
|
|
|
|
|
}
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
#region Commands
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request);
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
2026-04-03 19:50:52 +02:00
|
|
|
private async Task ExecuteTopBarAction(IMenuOperation menuItem)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
if (menuItem is not MailOperationMenuItem mailOperationMenuItem || MailCollection.SelectedItemsCount == 0) return;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
await HandleMailOperation(mailOperationMenuItem.Operation, MailCollection.SelectedItems);
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-05-08 02:05:42 +02: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)
|
|
|
|
|
{
|
2025-10-26 14:53:22 +01:00
|
|
|
if (MailCollection.SelectedItemsCount == 0) return;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
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>
|
2026-04-10 14:50:53 +02:00
|
|
|
/// Sends a new message to synchronize current folder.
|
2025-02-16 11:54:23 +01:00
|
|
|
/// </summary>
|
|
|
|
|
[RelayCommand]
|
2026-04-10 14:50:53 +02:00
|
|
|
private void SyncFolder()
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-10 14:50:53 +02:00
|
|
|
if (!CanSynchronize) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
//_notificationBuilder.CreateNotificationsAsync(MailCollection.SelectedItems.Select(a => a.MailCopy));
|
|
|
|
|
//return;
|
2026-04-16 01:32:48 +02:00
|
|
|
|
2026-04-10 14:50:53 +02:00
|
|
|
// 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.
|
|
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
trackingSynchronizationId = Guid.NewGuid();
|
|
|
|
|
completedTrackingSynchronizationCount = 0;
|
2026-04-16 01:32:48 +02:00
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
foreach (var folder in ActiveFolder.HandlingFolders)
|
|
|
|
|
{
|
|
|
|
|
var options = new MailSynchronizationOptions()
|
|
|
|
|
{
|
|
|
|
|
AccountId = folder.MailAccountId,
|
|
|
|
|
Type = MailSynchronizationType.CustomFolders,
|
|
|
|
|
SynchronizationFolderIds = [folder.Id],
|
|
|
|
|
GroupedSynchronizationTrackingId = trackingSynchronizationId
|
|
|
|
|
};
|
2026-04-16 01:32:48 +02:00
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
Messenger.Send(new NewMailSynchronizationRequested(options));
|
|
|
|
|
}
|
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);
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-22 00:22:00 +01:00
|
|
|
if (IsInSearchMode)
|
2024-05-08 02:05:42 +02:00
|
|
|
{
|
2025-02-22 00:22:00 +01:00
|
|
|
IsOnlineSearchButtonVisible = false;
|
2024-05-08 02:05:42 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-22 00:22:00 +01:00
|
|
|
await UpdateFolderPivotsAsync();
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task EnableFolderSynchronizationAsync()
|
|
|
|
|
{
|
2026-04-15 01:18:07 +02:00
|
|
|
if (ActiveFolder == null || IsCategoryView) return;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
foreach (var folder in ActiveFolder.HandlingFolders)
|
|
|
|
|
{
|
|
|
|
|
await _folderService.ChangeFolderSynchronizationStateAsync(folder.Id, true);
|
2024-05-08 02:05:42 +02:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2026-04-18 15:34:14 +02:00
|
|
|
[RelayCommand(CanExecute = nameof(CanEmptyFolder))]
|
|
|
|
|
private async Task EmptyFolderAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!IsJunkFolder || ActiveFolder == null) return;
|
|
|
|
|
|
|
|
|
|
foreach (var folder in ActiveFolder.HandlingFolders.OfType<MailItemFolder>())
|
|
|
|
|
{
|
|
|
|
|
var folderPrepRequest = new FolderOperationPreperationRequest(FolderOperation.Empty, folder);
|
|
|
|
|
await _winoRequestDelegator.ExecuteAsync(folderPrepRequest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool CanEmptyFolder() => IsJunkFolder && !IsAccountSynchronizerInSynchronization;
|
|
|
|
|
|
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;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-10-30 17:15:05 +01:00
|
|
|
Debug.WriteLine("Loading more...");
|
2025-02-16 11:54:23 +01:00
|
|
|
await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
2024-05-09 00:51:16 +02:00
|
|
|
|
2026-04-15 01:18:07 +02:00
|
|
|
var initializationOptions = CreateInitializationOptions(
|
|
|
|
|
IsInSearchMode ? SearchQuery : string.Empty,
|
|
|
|
|
MailCollection.MailCopyIdHashSet);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01: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);
|
2026-03-01 12:07:15 +01:00
|
|
|
var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync().ConfigureAwait(false);
|
|
|
|
|
ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
await MailCollection.AddRangeAsync(viewModels, false);
|
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
|
|
|
#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)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-03-08 13:21:42 +01:00
|
|
|
if (args.Handled || args.Mode != WinoApplicationMode.Mail)
|
|
|
|
|
return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
var targetItems = GetShortcutTargetItems().ToList();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
switch (args.Action)
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2026-03-08 13:21:42 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
private IEnumerable<MailItemViewModel> GetShortcutTargetItems()
|
|
|
|
|
{
|
|
|
|
|
if (MailCollection.SelectedItemsCount > 0)
|
|
|
|
|
return MailCollection.SelectedItems.OfType<MailItemViewModel>();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
if (_activeMailItem != null)
|
|
|
|
|
return [_activeMailItem];
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
return [];
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
private async Task CreateReplyDraftAsync(DraftCreationReason reason)
|
|
|
|
|
{
|
|
|
|
|
var targetMail = GetShortcutTargetItems().FirstOrDefault();
|
|
|
|
|
if (targetMail?.MailCopy == null || targetMail.MailCopy.FileId == Guid.Empty)
|
|
|
|
|
return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2024-08-13 22:54:36 +02:00
|
|
|
|
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
|
|
|
|
2026-04-15 01:18:07 +02:00
|
|
|
public async Task<(IReadOnlyList<MailCategory> Categories, IReadOnlyCollection<Guid> AssignedCategoryIds)> GetAvailableCategoriesAsync(IEnumerable<MailItemViewModel> targetItems)
|
|
|
|
|
{
|
|
|
|
|
var targetList = targetItems?.Where(a => a?.MailCopy?.AssignedAccount != null).ToList() ?? [];
|
|
|
|
|
if (targetList.Count == 0)
|
|
|
|
|
return ([], []);
|
|
|
|
|
|
|
|
|
|
var accountIds = targetList.Select(a => a.MailCopy.AssignedAccount.Id).Distinct().ToList();
|
|
|
|
|
if (accountIds.Count != 1)
|
|
|
|
|
return ([], []);
|
|
|
|
|
|
|
|
|
|
var accountId = accountIds[0];
|
|
|
|
|
var uniqueIds = targetList.Select(a => a.MailCopy.UniqueId).Distinct().ToList();
|
|
|
|
|
|
|
|
|
|
var categories = await _mailCategoryService.GetCategoriesAsync(accountId).ConfigureAwait(false);
|
|
|
|
|
var assignedCategoryIds = await _mailCategoryService.GetAssignedCategoryIdsForAllAsync(uniqueIds).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
return (categories, assignedCategoryIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task ToggleCategoryAssignmentAsync(MailCategory category, IEnumerable<MailItemViewModel> targetItems, bool isAssignedToAll)
|
|
|
|
|
{
|
|
|
|
|
var targetList = targetItems?.Where(a => a?.MailCopy?.AssignedAccount != null).ToList() ?? [];
|
|
|
|
|
if (category == null || targetList.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var accountIds = targetList.Select(a => a.MailCopy.AssignedAccount.Id).Distinct().ToList();
|
|
|
|
|
if (accountIds.Count != 1)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var accountId = accountIds[0];
|
|
|
|
|
var uniqueIds = targetList.Select(a => a.MailCopy.UniqueId).Distinct().ToList();
|
|
|
|
|
|
|
|
|
|
if (isAssignedToAll)
|
|
|
|
|
{
|
|
|
|
|
await _mailCategoryService.UnassignCategoryAsync(category.Id, uniqueIds).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await _mailCategoryService.AssignCategoryAsync(category.Id, uniqueIds).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetList.First().MailCopy.AssignedAccount.ProviderType != MailProviderType.Outlook)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var requests = new List<IRequestBase>();
|
2026-04-20 02:18:23 +02:00
|
|
|
foreach (var mailItem in targetList.Select(a => a.MailCopy).GroupBy(a => a.UniqueId).Select(group => group.First()))
|
2026-04-15 01:18:07 +02:00
|
|
|
{
|
|
|
|
|
var categoryNames = await _mailCategoryService.GetCategoryNamesForMailAsync(mailItem.UniqueId).ConfigureAwait(false);
|
|
|
|
|
requests.Add(new MailCategoryAssignmentRequest(mailItem, category.Id, category.Name, categoryNames, !isAssignedToAll));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _winoRequestDelegator.ExecuteAsync(accountId, requests).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 23:17:08 +02:00
|
|
|
public Task ChangePinnedStatusAsync(IEnumerable<MailItemViewModel> targetItems, bool isPinned)
|
|
|
|
|
{
|
|
|
|
|
var uniqueIds = targetItems?
|
|
|
|
|
.Where(a => a?.MailCopy != null)
|
|
|
|
|
.Select(a => a.MailCopy.UniqueId)
|
|
|
|
|
.Distinct()
|
|
|
|
|
.ToList() ?? [];
|
|
|
|
|
|
|
|
|
|
return _mailService.ChangePinnedStatusAsync(uniqueIds, isPinned);
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
2026-04-15 01:18:07 +02:00
|
|
|
=> !IsCategoryView && mailItem?.AssignedFolder != null && ActiveFolder?.HandlingFolders?.Any(a => a.Id == mailItem.AssignedFolder.Id) == true;
|
2026-02-14 12:52:17 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
public void RemoveFirst()
|
|
|
|
|
{
|
|
|
|
|
var fi = MailCollection.GetFirst();
|
2025-10-28 16:47:06 +01:00
|
|
|
if (fi == null) return;
|
2025-10-26 14:53:22 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
Messenger.Send(new MailRemovedMessage(fi.MailCopy, EntityUpdateSource.Server));
|
2025-10-26 14:53:22 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-31 11:26:51 +01:00
|
|
|
/// <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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
protected override async void OnMailAdded(MailCopy addedMail, EntityUpdateSource source)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
base.OnMailAdded(addedMail, source);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (addedMail.AssignedAccount == null || addedMail.AssignedFolder == null) return;
|
2024-08-25 02:01:08 +02:00
|
|
|
|
2026-04-06 11:21:51 +02:00
|
|
|
bool hasLock = false;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
try
|
|
|
|
|
{
|
2026-04-06 11:21:51 +02:00
|
|
|
await listManipulationSemepahore.WaitAsync();
|
|
|
|
|
hasLock = true;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (ActiveFolder == null) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-06 11:21:51 +02:00
|
|
|
// Re-evaluate folder membership after acquiring the semaphore so an add that was queued
|
|
|
|
|
// behind a folder re-initialization cannot land in the newly selected folder by mistake.
|
2025-02-16 11:54:23 +01:00
|
|
|
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))
|
2025-10-31 11:26:51 +01:00
|
|
|
{
|
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;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-14 12:52:17 +01:00
|
|
|
foreach (var group in MailCollection.MailItems)
|
2026-02-07 13:10:57 +01:00
|
|
|
{
|
2026-02-14 12:52:17 +01:00
|
|
|
foreach (var item in group)
|
2026-02-07 13:10:57 +01:00
|
|
|
{
|
2026-02-14 12:52:17 +01:00
|
|
|
if (item is MailItemViewModel mailItem)
|
2026-02-07 13:10:57 +01:00
|
|
|
{
|
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-07 13:10:57 +01:00
|
|
|
{
|
2026-02-14 12:52:17 +01:00
|
|
|
hasLocalDraftInSameThread = true;
|
|
|
|
|
break;
|
2026-02-07 13:10:57 +01:00
|
|
|
}
|
2026-02-14 12:52:17 +01:00
|
|
|
}
|
|
|
|
|
else if (item is ThreadMailItemViewModel threadItem)
|
|
|
|
|
{
|
|
|
|
|
foreach (var threadEmail in threadItem.ThreadEmails)
|
2026-02-07 13:10:57 +01:00
|
|
|
{
|
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-07 13:10:57 +01:00
|
|
|
{
|
2026-02-14 12:52:17 +01:00
|
|
|
hasLocalDraftInSameThread = true;
|
|
|
|
|
break;
|
2026-02-07 13:10:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 12:52:17 +01:00
|
|
|
if (hasLocalDraftInSameThread) break;
|
2026-02-07 13:10:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 12:52:17 +01:00
|
|
|
if (hasLocalDraftInSameThread) break;
|
2026-02-07 13:10:57 +01:00
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-10-31 11:26:51 +01:00
|
|
|
}
|
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;
|
|
|
|
|
}
|
2024-08-31 13:25:55 +02:00
|
|
|
|
2025-10-31 00:51:27 +01:00
|
|
|
// AddAsync already handles UI threading internally, no need to wrap it
|
2025-02-16 11:54:23 +01:00
|
|
|
await MailCollection.AddAsync(addedMail);
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
if (source == EntityUpdateSource.ClientUpdated)
|
|
|
|
|
{
|
|
|
|
|
var addedContainer = MailCollection.GetMailItemContainer(addedMail.UniqueId);
|
|
|
|
|
if (addedContainer?.ItemViewModel != null)
|
|
|
|
|
{
|
|
|
|
|
addedContainer.ItemViewModel.IsBusy = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-31 00:51:27 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2025-10-21 22:08:56 +02:00
|
|
|
{
|
|
|
|
|
NotifyItemFoundState();
|
|
|
|
|
});
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
finally
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2026-04-06 11:21:51 +02:00
|
|
|
if (hasLock)
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
protected override async void OnMailUpdated(MailCopy updatedMail, EntityUpdateSource source, MailCopyChangeFlags changedProperties)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-03-01 12:07:15 +01:00
|
|
|
base.OnMailUpdated(updatedMail, source, changedProperties);
|
2026-02-11 11:34:50 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-12 18:57:55 +01:00
|
|
|
bool isItemListed = MailCollection.ContainsMailUniqueId(updatedMail.UniqueId);
|
|
|
|
|
if (!isItemListed) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-12 18:57:55 +01:00
|
|
|
if (ShouldRemoveUpdatedMailFromCurrentList(updatedMail))
|
|
|
|
|
{
|
|
|
|
|
await MailCollection.RemoveAsync(updatedMail);
|
|
|
|
|
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 12:07:15 +01:00
|
|
|
await MailCollection.UpdateMailCopy(updatedMail, source, changedProperties);
|
2026-02-11 11:34:50 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-05 16:53:51 +02:00
|
|
|
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
2026-01-28 03:25:05 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
protected override async void OnMailStateUpdated(MailStateChange updatedState, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
base.OnMailStateUpdated(updatedState, source);
|
|
|
|
|
|
|
|
|
|
if (updatedState == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
|
|
|
|
|
|
|
|
|
if (!MailCollection.ContainsMailUniqueId(updatedState.UniqueId))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await MailCollection.UpdateMailStateAsync(updatedState, source);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async void OnBulkMailStateUpdated(IReadOnlyList<MailStateChange> updatedStates, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
var targetStates = updatedStates?
|
|
|
|
|
.Where(x => x != null)
|
|
|
|
|
.GroupBy(x => x.UniqueId)
|
|
|
|
|
.Select(group => group.Last())
|
|
|
|
|
.ToList() ?? [];
|
|
|
|
|
|
|
|
|
|
if (targetStates.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
|
|
|
|
|
|
|
|
|
var listedStates = targetStates
|
|
|
|
|
.Where(state => MailCollection.ContainsMailUniqueId(state.UniqueId))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (listedStates.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await MailCollection.UpdateMailStatesAsync(listedStates, source);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async void OnBulkMailUpdated(IReadOnlyList<MailCopy> updatedMails, EntityUpdateSource source, MailCopyChangeFlags changedProperties)
|
|
|
|
|
{
|
|
|
|
|
var targetMails = updatedMails?
|
|
|
|
|
.Where(x => x != null)
|
|
|
|
|
.GroupBy(x => x.UniqueId)
|
|
|
|
|
.Select(group => group.First())
|
|
|
|
|
.ToList() ?? [];
|
|
|
|
|
|
|
|
|
|
if (targetMails.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
|
|
|
|
|
|
|
|
|
var listedMails = targetMails
|
|
|
|
|
.Where(mail => MailCollection.ContainsMailUniqueId(mail.UniqueId))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (listedMails.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var mailsToRemove = listedMails
|
|
|
|
|
.Where(ShouldRemoveUpdatedMailFromCurrentList)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var mailIdsToRemove = mailsToRemove.Select(x => x.UniqueId).ToHashSet();
|
|
|
|
|
var mailsToUpdate = listedMails
|
|
|
|
|
.Where(mail => !mailIdsToRemove.Contains(mail.UniqueId))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (mailsToRemove.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
await MailCollection.RemoveRangeAsync(mailsToRemove);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mailsToUpdate.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
await MailCollection.UpdateMailCopiesAsync(mailsToUpdate, source, changedProperties);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
NotifyItemFoundState();
|
|
|
|
|
SetupTopBarActions();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
base.OnMailRemoved(removedMail, source);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
if (removedMail.AssignedAccount == null) return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-11 11:34:50 +01: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
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
bool isDeletedByGmailUnreadFolderAction = ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
|
|
|
|
|
gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
if (removedItemExistsInCurrentList && !isDeletedByGmailUnreadFolderAction)
|
|
|
|
|
{
|
|
|
|
|
MailItemViewModel nextItem = null;
|
2026-04-20 02:18:23 +02:00
|
|
|
bool isDeletedMailSelected = false;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
await ExecuteUIThread(() =>
|
2025-02-23 17:05:46 +01:00
|
|
|
{
|
2026-04-20 02:18:23 +02:00
|
|
|
isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
|
|
|
|
|
|
|
|
|
|
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
|
2026-02-11 11:34:50 +01:00
|
|
|
{
|
|
|
|
|
nextItem = MailCollection.GetNextItem(removedMail);
|
2026-04-20 02:18:23 +02:00
|
|
|
}
|
|
|
|
|
});
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
// RemoveAsync already handles UI threading internally
|
|
|
|
|
await MailCollection.RemoveAsync(removedMail);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
if (nextItem != null)
|
|
|
|
|
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem.UniqueId, ScrollToItem: true));
|
|
|
|
|
else if (isDeletedMailSelected)
|
2025-02-23 17:05:46 +01:00
|
|
|
{
|
2026-02-11 11:34:50 +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:35:43 +01:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
// UnselectAllAsync already handles UI threading internally
|
|
|
|
|
await MailCollection.UnselectAllAsync();
|
|
|
|
|
}
|
2025-02-16 11:43:30 +01:00
|
|
|
|
2026-02-11 11:34:50 +01:00
|
|
|
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
|
|
|
|
}
|
|
|
|
|
else if (isDeletedByGmailUnreadFolderAction)
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
2026-02-11 11:34:50 +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
|
|
|
}
|
2026-02-11 11:34:50 +01:00
|
|
|
finally
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2026-02-11 11:34:50 +01:00
|
|
|
listManipulationSemepahore.Release();
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-20 02:18:23 +02:00
|
|
|
protected override async void OnBulkMailRemoved(IReadOnlyList<MailCopy> removedMails, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
var targetMails = removedMails?
|
|
|
|
|
.Where(x => x != null && x.AssignedAccount != null)
|
|
|
|
|
.GroupBy(x => x.UniqueId)
|
|
|
|
|
.Select(group => group.First())
|
|
|
|
|
.ToList() ?? [];
|
|
|
|
|
|
|
|
|
|
if (targetMails.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
|
|
|
|
|
|
|
|
|
var existingMails = targetMails
|
|
|
|
|
.Where(mail => MailCollection.ContainsMailUniqueId(mail.UniqueId))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (existingMails.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var removedMailIds = existingMails.Select(mail => mail.UniqueId).ToHashSet();
|
|
|
|
|
var shouldClearSelection = false;
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
shouldClearSelection = MailCollection.SelectedItems.Any(item => removedMailIds.Contains(item.MailCopy.UniqueId));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await MailCollection.RemoveRangeAsync(existingMails);
|
|
|
|
|
|
|
|
|
|
if (shouldClearSelection)
|
|
|
|
|
{
|
|
|
|
|
await MailCollection.UnselectAllAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
NotifyItemFoundState();
|
|
|
|
|
SetupTopBarActions();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async void OnBulkMailAdded(IReadOnlyList<MailCopy> addedMails, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
var targetMails = addedMails?
|
|
|
|
|
.Where(x => x != null)
|
|
|
|
|
.GroupBy(x => x.UniqueId)
|
|
|
|
|
.Select(group => group.First())
|
|
|
|
|
.ToList() ?? [];
|
|
|
|
|
|
|
|
|
|
if (targetMails.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await listManipulationSemepahore.WaitAsync();
|
|
|
|
|
|
|
|
|
|
var mailsToAdd = new List<MailCopy>();
|
|
|
|
|
|
|
|
|
|
foreach (var addedMail in targetMails)
|
|
|
|
|
{
|
|
|
|
|
if (MailCollection.ContainsMailUniqueId(addedMail.UniqueId))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (!ShouldIncludeAddedMailInCurrentList(addedMail))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (ShouldPreventItemAdd(addedMail))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (SelectedFolderPivot?.IsFocused is bool isFocused && addedMail.IsFocused != isFocused)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (IsInSearchMode)
|
|
|
|
|
{
|
|
|
|
|
if (IsOnlineSearchEnabled || AreSearchResultsOnline)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (!IsMailMatchingLocalSearch(addedMail))
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mailsToAdd.Add(addedMail);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mailsToAdd.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await MailCollection.AddRangeAsync(mailsToAdd.Select(mail => new MailItemViewModel(mail)), false);
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
NotifyItemFoundState();
|
|
|
|
|
SetupTopBarActions();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
listManipulationSemepahore.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
2025-02-16 11:35:43 +01:00
|
|
|
{
|
2026-02-08 22:20:38 +01:00
|
|
|
await MailCollection.ClearAsync();
|
2025-02-16 11:35:43 +01: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
|
|
|
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
|
2025-02-16 11:54:23 +01:00
|
|
|
await MailCollection.AddAsync(draftMail);
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
2025-02-16 11:43:30 +01:00
|
|
|
{
|
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
|
|
|
|
2026-02-07 13:10:57 +01:00
|
|
|
protected override void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId)
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2026-02-07 13:10:57 +01:00
|
|
|
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
|
|
|
{
|
2026-04-22 01:27:48 +02:00
|
|
|
await PopulateMailCategoriesAsync(mailItems, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
2025-10-31 01:41:51 +01:00
|
|
|
// Run ViewModel creation on background thread to avoid blocking UI
|
|
|
|
|
return await Task.Run(() =>
|
2025-02-16 11:54:23 +01:00
|
|
|
{
|
2025-10-31 01:41:51 +01:00
|
|
|
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
|
|
|
|
2026-04-22 01:27:48 +02:00
|
|
|
private async Task PopulateMailCategoriesAsync(IEnumerable<MailCopy> mailItems, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var mails = mailItems?.Where(a => a != null).ToList() ?? [];
|
|
|
|
|
if (mails.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var accountIdsByFolderId = ActiveFolder?.HandlingFolders?
|
|
|
|
|
.GroupBy(a => a.Id)
|
|
|
|
|
.ToDictionary(a => a.Key, a => a.First().MailAccountId) ?? new Dictionary<Guid, Guid>();
|
|
|
|
|
|
|
|
|
|
var mailsByAccount = mails
|
|
|
|
|
.GroupBy(mail => ResolveMailAccountId(mail, accountIdsByFolderId))
|
|
|
|
|
.Where(group => group.Key != Guid.Empty)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
foreach (var groupedMails in mailsByAccount)
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
var categoriesByMail = await _mailCategoryService
|
|
|
|
|
.GetCategoriesByMailAsync(groupedMails.Key, groupedMails.Select(a => a.UniqueId))
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
foreach (var mail in groupedMails)
|
|
|
|
|
{
|
|
|
|
|
mail.Categories = categoriesByMail.TryGetValue(mail.UniqueId, out var categories)
|
|
|
|
|
? categories.ToList()
|
|
|
|
|
: [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 12:07:15 +01: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-16 11:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-15 01:18:07 +02:00
|
|
|
private MailListInitializationOptions CreateInitializationOptions(
|
|
|
|
|
string searchQuery,
|
|
|
|
|
System.Collections.Concurrent.ConcurrentDictionary<Guid, bool> existingUniqueIds,
|
|
|
|
|
List<MailCopy> preFetchedMailCopies = null,
|
|
|
|
|
bool deduplicateByServerId = false)
|
|
|
|
|
{
|
|
|
|
|
var options = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
|
|
|
|
SelectedFilterOption.Type,
|
|
|
|
|
SelectedSortingOption.Type,
|
|
|
|
|
PreferencesService.IsThreadingEnabled,
|
|
|
|
|
SelectedFolderPivot.IsFocused,
|
|
|
|
|
searchQuery,
|
|
|
|
|
existingUniqueIds,
|
|
|
|
|
preFetchedMailCopies,
|
|
|
|
|
DeduplicateByServerId: deduplicateByServerId);
|
|
|
|
|
|
|
|
|
|
if (!IsCategoryView)
|
|
|
|
|
return options;
|
|
|
|
|
|
|
|
|
|
var categoryIds = ActiveFolder switch
|
|
|
|
|
{
|
|
|
|
|
IMailCategoryMenuItem singleCategoryMenuItem => new List<Guid> { singleCategoryMenuItem.MailCategory.Id },
|
|
|
|
|
IMergedMailCategoryMenuItem mergedCategoryMenuItem => mergedCategoryMenuItem.Categories.Select(a => a.Id).ToList(),
|
|
|
|
|
_ => []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return options with
|
|
|
|
|
{
|
|
|
|
|
CategoryIds = categoryIds
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
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 [];
|
|
|
|
|
|
2026-04-12 15:56:27 +02:00
|
|
|
var distinctFolders = handlingFolders
|
|
|
|
|
.Where(folder => folder != null)
|
|
|
|
|
.GroupBy(folder => folder.Id)
|
|
|
|
|
.Select(group => group.First())
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var foldersByAccount = distinctFolders
|
2026-02-12 18:57:55 +01:00
|
|
|
.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);
|
|
|
|
|
|
2026-04-12 15:56:27 +02:00
|
|
|
var accountIdsByFolderId = distinctFolders.ToDictionary(folder => folder.Id, folder => folder.MailAccountId);
|
|
|
|
|
var preferredFolderIds = distinctFolders.Select(folder => folder.Id).ToHashSet();
|
|
|
|
|
|
|
|
|
|
return DeduplicateOnlineSearchResults(allResults.SelectMany(a => a), accountIdsByFolderId, preferredFolderIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<MailCopy> DeduplicateOnlineSearchResults(IEnumerable<MailCopy> results,
|
|
|
|
|
IReadOnlyDictionary<Guid, Guid> accountIdsByFolderId,
|
|
|
|
|
ISet<Guid> preferredFolderIds)
|
|
|
|
|
{
|
|
|
|
|
if (results == null) return [];
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
.Where(mail => mail != null)
|
|
|
|
|
.GroupBy(mail => (ResolveMailAccountId(mail, accountIdsByFolderId), ResolveSearchMailId(mail)))
|
|
|
|
|
.Select(group => group
|
|
|
|
|
.OrderByDescending(mail => preferredFolderIds.Contains(mail.FolderId))
|
|
|
|
|
.ThenByDescending(mail => mail.CreationDate)
|
|
|
|
|
.ThenBy(mail => mail.FolderId)
|
|
|
|
|
.ThenBy(mail => mail.UniqueId)
|
|
|
|
|
.First())
|
2026-02-12 18:57:55 +01:00
|
|
|
.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 15:56:27 +02:00
|
|
|
private static Guid ResolveMailAccountId(MailCopy mail, IReadOnlyDictionary<Guid, Guid> accountIdsByFolderId)
|
|
|
|
|
{
|
|
|
|
|
if (mail?.AssignedAccount != null)
|
|
|
|
|
return mail.AssignedAccount.Id;
|
|
|
|
|
|
|
|
|
|
if (mail != null && accountIdsByFolderId.TryGetValue(mail.FolderId, out var accountId))
|
|
|
|
|
return accountId;
|
|
|
|
|
|
|
|
|
|
return Guid.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ResolveSearchMailId(MailCopy mail)
|
|
|
|
|
=> string.IsNullOrWhiteSpace(mail?.Id) ? mail?.UniqueId.ToString("N") ?? string.Empty : mail.Id;
|
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private async Task InitializeFolderAsync()
|
|
|
|
|
{
|
|
|
|
|
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
|
|
|
|
|
return;
|
2024-08-25 02:01:08 +02:00
|
|
|
|
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
|
|
|
|
2026-04-11 10:54:14 +02:00
|
|
|
MailCollection.PruneSingleNonDraftItems = IsActiveDraftFolder();
|
|
|
|
|
|
2025-02-16 11:54:23 +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;
|
2025-02-22 00:22:00 +01:00
|
|
|
List<MailCopy> onlineSearchItems = null;
|
|
|
|
|
|
|
|
|
|
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
|
|
|
|
|
bool isDoingOnlineSearch = false;
|
|
|
|
|
|
|
|
|
|
if (isDoingSearch)
|
|
|
|
|
{
|
|
|
|
|
isDoingOnlineSearch = PreferencesService.DefaultSearchMode == SearchMode.Online || IsOnlineSearchEnabled;
|
|
|
|
|
|
|
|
|
|
// Perform online search.
|
|
|
|
|
if (isDoingOnlineSearch)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-02-12 18:57:55 +01:00
|
|
|
onlineSearchItems = await PerformSynchronizerOnlineSearchAsync(SearchQuery, ActiveFolder.HandlingFolders, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
await ExecuteUIThread(() => { AreSearchResultsOnline = true; });
|
2025-02-22 00:22:00 +01:00
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Warning(ex, "Failed to perform online search.");
|
|
|
|
|
|
2026-02-12 18:57:55 +01:00
|
|
|
isDoingOnlineSearch = false;
|
|
|
|
|
onlineSearchItems = null;
|
2025-02-22 00:22:00 +01:00
|
|
|
|
2026-02-12 18:57:55 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 23:17:08 +02:00
|
|
|
var initialExistingIds = new ConcurrentDictionary<Guid, bool>(MailCollection.MailCopyIdHashSet);
|
|
|
|
|
var localPinnedItems = new List<MailCopy>();
|
|
|
|
|
|
|
|
|
|
if (!isDoingOnlineSearch)
|
|
|
|
|
{
|
|
|
|
|
var pinnedOptions = CreateInitializationOptions(SearchQuery, MailCollection.MailCopyIdHashSet);
|
|
|
|
|
localPinnedItems = await _mailService.FetchPinnedMailsAsync(pinnedOptions, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
foreach (var pinnedItem in localPinnedItems)
|
|
|
|
|
{
|
|
|
|
|
initialExistingIds.TryAdd(pinnedItem.UniqueId, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 01:18:07 +02:00
|
|
|
var initializationOptions = CreateInitializationOptions(
|
|
|
|
|
isDoingOnlineSearch ? string.Empty : SearchQuery,
|
2026-04-21 23:17:08 +02:00
|
|
|
initialExistingIds,
|
2026-04-15 01:18:07 +02:00
|
|
|
onlineSearchItems,
|
|
|
|
|
isDoingOnlineSearch);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-22 00:22:00 +01:00
|
|
|
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
|
2026-04-21 23:17:08 +02:00
|
|
|
items = localPinnedItems.Count > 0 ? [.. localPinnedItems, .. items] : items;
|
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);
|
2026-03-01 12:07:15 +01:00
|
|
|
var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
|
2025-10-31 01:41:51 +01:00
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
|
2025-02-16 11:35:43 +01:00
|
|
|
|
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();
|
2024-05-08 02:05:42 +02:00
|
|
|
|
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
|
2024-08-11 15:25:40 +02:00
|
|
|
|
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();
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Let awaiters know about the completion of mail init.
|
|
|
|
|
message.FolderInitLoadAwaitTask?.TrySetResult(true);
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
await Task.Yield();
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
isChangingFolder = false;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
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;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
bool isLinkedInboxSyncResult = message.SynchronizationTrackingId == trackingSynchronizationId;
|
2024-05-08 02:05:42 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (isLinkedInboxSyncResult)
|
2024-07-09 01:05:16 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
var isCompletedAccountListed = ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId);
|
2024-07-09 01:05:16 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
2024-07-09 01:05:16 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
bool isReportingActiveAccountResult = ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == message.AccountId);
|
2024-07-09 01:05:16 +02:00
|
|
|
|
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:
|
2026-04-20 23:06:11 +02:00
|
|
|
// No need to pop success message when executing requests all the time...
|
|
|
|
|
if (message.Type != MailSynchronizationType.ExecuteRequests)
|
|
|
|
|
{
|
|
|
|
|
UpdateBarMessage(InfoBarMessageType.Success, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Success);
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
break;
|
2026-02-07 19:47:21 +01:00
|
|
|
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)
|
|
|
|
|
{
|
2025-11-12 15:44:43 +01:00
|
|
|
// TODO: Remove this.
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-11-12 15:44:43 +01: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-02-22 23:09:53 +01:00
|
|
|
|
2025-10-31 00:51:27 +01:00
|
|
|
public async void Receive(AccountCacheResetMessage message)
|
2025-02-22 23:09:53 +01:00
|
|
|
{
|
|
|
|
|
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-02-22 23:09:53 +01:00
|
|
|
|
2025-10-31 00:51:27 +01:00
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
2025-02-22 23:09:53 +01:00
|
|
|
_mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-21 01:40:25 +02:00
|
|
|
|
2025-10-26 14:53:22 +01:00
|
|
|
protected override void OnDispatcherAssigned()
|
|
|
|
|
{
|
|
|
|
|
base.OnDispatcherAssigned();
|
|
|
|
|
|
|
|
|
|
MailCollection.CoreDispatcher = Dispatcher;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 15:46:38 +02:00
|
|
|
public void Receive(ThumbnailAdded message)
|
|
|
|
|
{
|
2025-10-26 14:53:22 +01:00
|
|
|
_ = MailCollection.UpdateThumbnailsForAddressAsync(message.Email);
|
2025-10-03 15:46:38 +02:00
|
|
|
}
|
2025-10-21 01:27:29 +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);
|
2025-10-21 01:27:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-21 01:27:29 +02:00
|
|
|
}
|
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-28 16:47:06 +01:00
|
|
|
|
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);
|
|
|
|
|
}
|
2026-04-20 02:18:23 +02:00
|
|
|
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|