Fixing some issues with ItemsView and selections.

This commit is contained in:
Burak Kaan Köse
2025-10-21 22:08:56 +02:00
parent ae7d576967
commit 449c1d3f4d
11 changed files with 657 additions and 187 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor
var requests = new List<IMailActionRequest>();
// TODO: Fix: Collection was modified; enumeration operation may not execute
foreach (var item in preperationRequest.MailItems)
foreach (var item in preperationRequest.MailItems.ToList())
{
var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution);
@@ -6,6 +6,7 @@ using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Wino.Core.Domain.Entities.Mail;
using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Collections;
@@ -34,12 +35,15 @@ public enum EmailSortDirection
/// </summary>
public partial class GroupedEmailCollection : ObservableObject, IRecipient<PropertyChangedMessage<bool>>, IDisposable
{
public event EventHandler SelectionChanged;
private readonly ObservableCollection<MailItemViewModel> _sourceItems = [];
private readonly Dictionary<string, GroupHeaderBase> _groupHeaders = [];
private readonly Dictionary<string, int> _groupHeaderIndexCache = [];
private readonly Dictionary<string, List<object>> _groupItems = [];
private readonly Dictionary<string, ThreadMailItemViewModel> _threadExpanders = [];
private readonly HashSet<Guid> _mailCopyIdHashSet = [];
private readonly HashSet<MailItemViewModel> _selectedVisibleItems = [];
private bool _disposed;
private bool _isUpdating;
@@ -49,6 +53,28 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
[ObservableProperty]
private EmailSortDirection sortDirection = EmailSortDirection.Descending;
// Tracks the number of currently selected visible mail items. Notify derived bools when changed.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasSelectedItems))]
[NotifyPropertyChangedFor(nameof(HasSingleItemSelected))]
[NotifyPropertyChangedFor(nameof(HasMultipleItemsSelected))]
public partial int SelectedVisibleCount { get; set; }
/// <summary>
/// Indicates whether there are any selected visible items.
/// </summary>
public bool HasSelectedItems => SelectedVisibleCount > 0;
/// <summary>
/// Indicates whether there is exactly one selected visible item.
/// </summary>
public bool HasSingleItemSelected => SelectedVisibleCount == 1;
/// <summary>
/// Indicates whether there are multiple selected visible items.
/// </summary>
public bool HasMultipleItemsSelected => SelectedVisibleCount > 1;
public GroupedEmailCollection()
{
// Create a flat collection for ItemsView with headers, expanders and emails mixed
@@ -89,6 +115,12 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
/// </summary>
public IEnumerable<MailItemViewModel> AllItems => _sourceItems;
/// <summary>
/// Gets all currently selected visible email items in the UI.
/// This collection is automatically maintained by tracking PropertyChanged events.
/// </summary>
public IReadOnlyCollection<MailItemViewModel> SelectedVisibleItems => _selectedVisibleItems;
/// <summary>
/// Gets all currently selected email items.
/// Includes:
@@ -163,7 +195,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
}
/// <summary>
/// Handles PropertyChanged messages for thread expansion
/// Handles PropertyChanged messages for thread expansion and mail item selection
/// </summary>
public void Receive(PropertyChangedMessage<bool> message)
{
@@ -175,6 +207,94 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
{
HandleThreadExpansion(expander);
}
else if (message.PropertyName == nameof(MailItemViewModel.IsSelected) && message.Sender is MailItemViewModel mailItem)
{
HandleMailItemSelectionChanged(mailItem, message.NewValue);
}
}
private void HandleMailItemSelectionChanged(MailItemViewModel mailItem, bool isSelected)
{
bool selectionChanged = false;
if (isSelected)
{
// Add to selected items if it's visible in the UI
if (Items.Contains(mailItem))
{
if (_selectedVisibleItems.Add(mailItem))
{
SelectedVisibleCount = _selectedVisibleItems.Count;
OnPropertyChanged(nameof(SelectedVisibleItems));
selectionChanged = true;
}
}
}
else
{
// Remove from selected items
if (_selectedVisibleItems.Remove(mailItem))
{
SelectedVisibleCount = _selectedVisibleItems.Count;
OnPropertyChanged(nameof(SelectedVisibleItems));
selectionChanged = true;
}
}
if (selectionChanged)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Registers a mail item to track its selection state when added to the visible UI
/// </summary>
private void RegisterMailItemForSelectionTracking(MailItemViewModel mailItem)
{
if (mailItem == null)
return;
// Subscribe to property changed to track IsSelected changes
mailItem.PropertyChanged += MailItem_PropertyChanged;
// If the item is already selected, add it to the tracking set
if (mailItem.IsSelected && Items.Contains(mailItem))
{
if (_selectedVisibleItems.Add(mailItem))
{
SelectedVisibleCount = _selectedVisibleItems.Count;
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <summary>
/// Unregisters a mail item from selection tracking when removed from the visible UI
/// </summary>
private void UnregisterMailItemFromSelectionTracking(MailItemViewModel mailItem)
{
if (mailItem == null)
return;
// Unsubscribe from property changed
mailItem.PropertyChanged -= MailItem_PropertyChanged;
// Remove from selected items tracking
if (_selectedVisibleItems.Remove(mailItem))
{
SelectedVisibleCount = _selectedVisibleItems.Count;
OnPropertyChanged(nameof(SelectedVisibleItems));
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
private void MailItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is MailItemViewModel mailItem && e.PropertyName == nameof(MailItemViewModel.IsSelected))
{
HandleMailItemSelectionChanged(mailItem, mailItem.IsSelected);
}
}
private void HandleThreadExpansion(ThreadMailItemViewModel expander)
@@ -197,6 +317,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var email in sortedThreadEmails)
{
Items.Insert(insertIndex, email);
RegisterMailItemForSelectionTracking(email);
insertIndex++;
}
@@ -210,8 +331,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var emailIndex = Items.IndexOf(email);
if (emailIndex >= 0)
{
var sourceItem = _sourceItems.FirstOrDefault(a => a == email);
UnregisterMailItemFromSelectionTracking(email);
Items.RemoveAt(emailIndex);
UpdateHeaderIndicesAfterRemoval(emailIndex);
}
@@ -315,17 +435,35 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
if (email?.MailCopy == null)
return;
RemoveEmailByMailCopy(email.MailCopy);
}
/// <summary>
/// Removes an email from the collection based on MailCopy.
/// The mail copy might be in a thread where it's not visible in the UI items.
/// In that case, it will be removed from the all items source and the thread.
/// </summary>
public void RemoveEmailByMailCopy(MailCopy mailCopy)
{
if (mailCopy == null)
return;
// Remove from unique ID tracking
_mailCopyIdHashSet.Remove(email.MailCopy.UniqueId);
_mailCopyIdHashSet.Remove(mailCopy.UniqueId);
_isUpdating = true;
try
{
var threadId = email.MailCopy.ThreadId;
// Find the email in the source collection
var email = _sourceItems.FirstOrDefault(e => e.MailCopy.UniqueId == mailCopy.UniqueId);
if (email == null)
return; // Email not found
var threadId = mailCopy.ThreadId;
// Remove from source collection
if (!_sourceItems.Remove(email))
return; // Email not found
_sourceItems.Remove(email);
if (!string.IsNullOrEmpty(threadId) && _threadExpanders.TryGetValue(threadId, out var expander))
{
@@ -333,7 +471,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
expander.RemoveEmail(email);
email.IsDisplayedInThread = false;
// Remove email from UI
// Remove email from UI if it's visible (only if thread is expanded)
RemoveEmailFromUI(email);
// Check if thread now has only 1 email - convert back to standalone
@@ -431,6 +569,20 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
_isUpdating = true;
try
{
// Unregister all mail items from selection tracking
foreach (var item in Items)
{
if (item is MailItemViewModel mailItem)
{
UnregisterMailItemFromSelectionTracking(mailItem);
}
}
// Clear selected items tracking
var hadSelectedItems = _selectedVisibleItems.Count > 0;
_selectedVisibleItems.Clear();
SelectedVisibleCount = 0;
// Reset IsDisplayedInThread for all emails before clearing
foreach (var email in _sourceItems)
{
@@ -453,6 +605,12 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
OnPropertyChanged(nameof(TotalCount));
OnPropertyChanged(nameof(TotalUnreadCount));
OnPropertyChanged(nameof(SelectedVisibleItems));
if (hadSelectedItems)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
finally
{
@@ -481,14 +639,32 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
_isUpdating = true;
try
{
// Unregister all mail items before clearing
foreach (var item in Items)
{
if (item is MailItemViewModel mailItem)
{
UnregisterMailItemFromSelectionTracking(mailItem);
}
}
// Clear UI items but preserve source and expanders
Items.Clear();
_groupHeaders.Clear();
_groupHeaderIndexCache.Clear();
_groupItems.Clear();
var hadSelectedItems = _selectedVisibleItems.Count > 0;
_selectedVisibleItems.Clear();
SelectedVisibleCount = 0;
if (!_sourceItems.Any())
{
if (hadSelectedItems)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
return;
}
// Rebuild thread expanders based on current emails
RebuildThreadExpanders();
@@ -550,6 +726,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var threadEmail in sortedThreadEmails)
{
Items.Add(threadEmail);
RegisterMailItemForSelectionTracking(threadEmail);
currentIndex++;
}
}
@@ -558,6 +735,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
{
// Add standalone email
Items.Add(email);
RegisterMailItemForSelectionTracking(email);
currentIndex++;
}
}
@@ -565,6 +743,11 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
// Update group header counts
UpdateAllGroupHeaderCounts();
if (hadSelectedItems)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
finally
{
@@ -595,27 +778,28 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
// Determine the group key for this email
var groupKey = GetGroupKeyForItem(email);
// Get or create the group header
var groupHeader = GetOrCreateGroupHeader(groupKey);
// Find where this email should be inserted in the UI
var insertPosition = FindUIInsertionPosition(email, groupKey);
// Insert the email at the correct position
Items.Insert(insertPosition, email);
RegisterMailItemForSelectionTracking(email);
// Update the group items list
if (!_groupItems.ContainsKey(groupKey))
{
_groupItems[groupKey] = new List<object>();
}
// Insert in the group items list maintaining sort order
var groupItems = _groupItems[groupKey];
var groupInsertIndex = FindGroupInsertionIndex(email, groupItems);
groupItems.Insert(groupInsertIndex, email);
// If this is the first item in a new group, we need to add the header
if (groupItems.Count == 1)
{
@@ -623,7 +807,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var headerInsertPosition = FindHeaderInsertionPosition(groupKey, groupHeader);
Items.Insert(headerInsertPosition, groupHeader);
_groupHeaderIndexCache[groupKey] = headerInsertPosition;
// Update all subsequent header indices
UpdateSubsequentHeaderIndices(groupKey, 1);
}
@@ -762,6 +946,196 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
return null;
}
/// <summary>
/// Gets the next item in the UI list based on the given MailCopy.
/// If the next item is a thread, the thread will be expanded and the first item in the thread returned.
/// </summary>
/// <param name="mailCopy">The mail copy to find the next item for.</param>
/// <returns>The next mail item in the UI list, or null if no next item exists.</returns>
public MailItemViewModel GetNextItem(MailCopy mailCopy)
{
if (mailCopy == null)
return null;
_isUpdating = true;
try
{
// Find the current item in the UI Items collection
var currentItemIndex = -1;
MailItemViewModel currentMailItem = null;
ThreadMailItemViewModel currentThread = null;
// First, try to find the item as a standalone email in the Items collection
for (int i = 0; i < Items.Count; i++)
{
var item = Items[i];
if (item is MailItemViewModel mailItem && mailItem.MailCopy.UniqueId == mailCopy.UniqueId)
{
currentItemIndex = i;
currentMailItem = mailItem;
break;
}
}
// If not found as standalone, check if it's in a thread
if (currentItemIndex == -1)
{
// Find the thread that contains this mail
foreach (var expander in _threadExpanders.Values)
{
if (expander.HasUniqueId(mailCopy.UniqueId))
{
// Find the thread expander in the Items collection
currentItemIndex = Items.IndexOf(expander);
currentThread = expander;
// If thread is expanded, find the specific mail item within the thread
if (expander.IsThreadExpanded)
{
var threadMailItem = expander.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == mailCopy.UniqueId);
if (threadMailItem != null)
{
currentItemIndex = Items.IndexOf(threadMailItem);
currentMailItem = threadMailItem;
}
}
break;
}
}
}
// If we still haven't found the item, it's not in the UI
if (currentItemIndex == -1)
return null;
// Look for the next item in the Items collection
for (int i = currentItemIndex + 1; i < Items.Count; i++)
{
var nextItem = Items[i];
// Skip group headers
if (nextItem is GroupHeaderBase)
continue;
// If next item is a mail item, return it
if (nextItem is MailItemViewModel nextMailItem)
{
return nextMailItem;
}
// If next item is a thread expander, expand it and return the first item
if (nextItem is ThreadMailItemViewModel threadExpander)
{
// Expand the thread if not already expanded
if (!threadExpander.IsThreadExpanded)
{
threadExpander.IsThreadExpanded = true;
}
// Return the first item in the thread (latest email)
var sortedThreadEmails = SortDirection == EmailSortDirection.Descending
? threadExpander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList()
: threadExpander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList();
return sortedThreadEmails.FirstOrDefault();
}
}
// No next item found
return null;
}
finally
{
_isUpdating = false;
}
}
/// <summary>
/// Searches for all mail items with a specific FromAddress and toggles their ThumbnailUpdatedEvent property.
/// This will notify the UI to update thumbnails for all matching items.
/// </summary>
/// <param name="fromAddress">The email address to search for in FromAddress property.</param>
public void UpdateThumbnailsForAddress(string fromAddress)
{
if (string.IsNullOrEmpty(fromAddress)) return;
// Search through all source items (includes both standalone and threaded emails)
foreach (var mailItem in _sourceItems)
{
if (string.Equals(mailItem.MailCopy.FromAddress, fromAddress, StringComparison.OrdinalIgnoreCase))
{
// Toggle the ThumbnailUpdatedEvent to notify the UI
mailItem.ThumbnailUpdatedEvent = !mailItem.ThumbnailUpdatedEvent;
}
}
}
/// <summary>
/// Selects all visible mail items in the collection.
/// Only operates on MailItemViewModel instances, skipping group headers and thread expanders.
/// </summary>
/// <returns>The number of items that were selected.</returns>
public int SelectAll()
{
var selectedCount = 0;
foreach (var item in Items)
{
// Only select MailItemViewModel instances (skip GroupHeaderBase and ThreadMailItemViewModel)
if (item is MailItemViewModel mailItem)
{
if (!mailItem.IsSelected)
{
mailItem.IsSelected = true;
selectedCount++;
}
}
}
SelectedVisibleCount = _selectedVisibleItems.Count;
if (selectedCount > 0)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
return selectedCount;
}
/// <summary>
/// Clears the selection of all visible mail items in the collection.
/// Only operates on MailItemViewModel instances, skipping group headers and thread expanders.
/// </summary>
/// <returns>The number of items that were deselected.</returns>
public int ClearSelections()
{
var deselectedCount = 0;
foreach (var item in Items)
{
// Only deselect MailItemViewModel instances (skip GroupHeaderBase and ThreadMailItemViewModel)
if (item is MailItemViewModel mailItem)
{
if (mailItem.IsSelected)
{
mailItem.IsSelected = false;
deselectedCount++;
}
}
}
SelectedVisibleCount = _selectedVisibleItems.Count;
if (deselectedCount > 0)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
return deselectedCount;
}
private void AddThreadToUI(ThreadMailItemViewModel expander, string groupKey)
{
var groupHeader = GetOrCreateGroupHeader(groupKey);
@@ -789,6 +1163,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var email in sortedThreadEmails)
{
Items.Insert(currentIndex, email);
RegisterMailItemForSelectionTracking(email);
currentIndex++;
totalInserted++;
}
@@ -819,6 +1194,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var email in sortedThreadEmails)
{
Items.Insert(currentIndex, email);
RegisterMailItemForSelectionTracking(email);
currentIndex++;
totalInserted++;
}
@@ -846,6 +1222,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var emailIndex = Items.IndexOf(email);
if (emailIndex >= 0)
{
UnregisterMailItemFromSelectionTracking(email);
Items.RemoveAt(emailIndex);
UpdateHeaderIndicesAfterRemoval(emailIndex);
}
@@ -857,6 +1234,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var itemIndex = Items.IndexOf(email);
if (itemIndex >= 0)
{
UnregisterMailItemFromSelectionTracking(email);
Items.RemoveAt(itemIndex);
UpdateHeaderIndicesAfterRemoval(itemIndex);
}
@@ -874,6 +1252,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var insertPosition = FindGroupInsertionPosition(groupKey);
Items.Insert(insertPosition, groupHeader);
Items.Insert(insertPosition + 1, email);
RegisterMailItemForSelectionTracking(email);
UpdateHeaderIndicesAfterInsertion(insertPosition, 2);
_groupHeaderIndexCache[groupKey] = insertPosition;
@@ -884,6 +1263,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var groupEndIndex = FindGroupEndIndex(headerIndex);
var insertIndex = FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex);
Items.Insert(insertIndex, email);
RegisterMailItemForSelectionTracking(email);
UpdateHeaderIndicesAfterInsertion(insertIndex);
}
@@ -1157,7 +1537,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var threadGroup in newThreadGroups)
{
var threadId = threadGroup.Key;
if (_threadExpanders.TryGetValue(threadId, out var existingExpander))
{
// Add new emails to existing thread
@@ -1175,13 +1555,13 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
.ToList();
var allThreadEmails = existingEmailsInThread.Concat(threadGroup).ToList();
if (allThreadEmails.Count >= 2)
{
// Create new thread expander
var expander = new ThreadMailItemViewModel(threadId);
_threadExpanders[threadId] = expander;
// Add all emails to the thread
foreach (var email in allThreadEmails)
{
@@ -1206,7 +1586,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var headerIndex = _groupHeaderIndexCache[groupKey];
var groupEndIndex = FindGroupEndIndex(headerIndex);
return FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex);
}
@@ -1216,19 +1596,19 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
private int FindGroupInsertionIndex(MailItemViewModel email, List<object> groupItems)
{
var emailDate = email.MailCopy?.CreationDate ?? DateTime.MinValue;
for (int i = 0; i < groupItems.Count; i++)
{
var existingDate = GetEffectiveDate(groupItems[i]);
var comparison = emailDate.CompareTo(existingDate);
if (SortDirection == EmailSortDirection.Descending)
comparison = -comparison;
if (comparison < 0)
return i;
}
return groupItems.Count;
}
@@ -1269,7 +1649,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
private void UpdateSubsequentHeaderIndices(string insertedGroupKey, int itemCount)
{
var insertedHeaderIndex = _groupHeaderIndexCache[insertedGroupKey];
var keysToUpdate = _groupHeaderIndexCache
.Where(kvp => kvp.Key != insertedGroupKey && kvp.Value > insertedHeaderIndex)
.Select(kvp => kvp.Key)
@@ -1321,6 +1701,15 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
{
_sourceItems.CollectionChanged -= OnSourceItemsChanged;
// Unregister all mail items from selection tracking
foreach (var item in Items)
{
if (item is MailItemViewModel mailItem)
{
UnregisterMailItemFromSelectionTracking(mailItem);
}
}
// Unregister from messenger
WeakReferenceMessenger.Default.Unregister<PropertyChangedMessage<bool>>(this);
@@ -1342,6 +1731,8 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
_groupHeaderIndexCache.Clear();
_groupItems.Clear();
_threadExpanders.Clear();
_selectedVisibleItems.Clear();
SelectedVisibleCount = 0;
}
_disposed = true;
@@ -0,0 +1,93 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Wino.Mail.ViewModels.Helpers;
/// <summary>
/// A throttled event handler that delays execution of a callback until a specified time has passed
/// without the event being triggered again. This is useful for scenarios where events fire rapidly
/// but you only want to handle the "final" event after a quiet period.
/// </summary>
public class ThrottledEventHandler : IDisposable
{
private readonly int _delayMilliseconds;
private readonly Func<Task> _asyncCallback;
private readonly Action _syncCallback;
private Timer _timer;
private volatile bool _disposed;
/// <summary>
/// Creates a new throttled event handler with a synchronous callback.
/// </summary>
/// <param name="delayMilliseconds">The delay in milliseconds to wait before executing the callback</param>
/// <param name="callback">The action to execute after the delay period</param>
public ThrottledEventHandler(int delayMilliseconds, Action callback)
{
_delayMilliseconds = delayMilliseconds;
_syncCallback = callback ?? throw new ArgumentNullException(nameof(callback));
}
/// <summary>
/// Creates a new throttled event handler with an asynchronous callback.
/// </summary>
/// <param name="delayMilliseconds">The delay in milliseconds to wait before executing the callback</param>
/// <param name="callback">The async function to execute after the delay period</param>
public ThrottledEventHandler(int delayMilliseconds, Func<Task> callback)
{
_delayMilliseconds = delayMilliseconds;
_asyncCallback = callback ?? throw new ArgumentNullException(nameof(callback));
}
/// <summary>
/// Triggers the throttled execution. If called again before the delay period expires,
/// the timer is reset and the callback execution is delayed further.
/// </summary>
public void Trigger()
{
if (_disposed) return;
// Dispose existing timer if it exists
_timer?.Dispose();
// Create new timer that will execute the callback after the delay
_timer = new Timer(ExecuteCallback, null, _delayMilliseconds, Timeout.Infinite);
}
private async void ExecuteCallback(object state)
{
if (_disposed) return;
try
{
if (_asyncCallback != null)
{
await _asyncCallback();
}
else
{
_syncCallback?.Invoke();
}
}
catch (Exception ex)
{
// Log error if logging is available, but don't crash
System.Diagnostics.Debug.WriteLine($"ThrottledEventHandler callback error: {ex}");
}
finally
{
// Dispose the timer since it's one-shot
_timer?.Dispose();
_timer = null;
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_timer?.Dispose();
_timer = null;
}
}
+84 -97
View File
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -21,11 +19,13 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Collections;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Helpers;
using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server;
@@ -36,8 +36,6 @@ namespace Wino.Mail.ViewModels;
public partial class MailListPageViewModel : MailBaseViewModel,
IRecipient<MailItemNavigationRequested>,
IRecipient<ActiveMailFolderChangedEvent>,
IRecipient<MailItemSelectedEvent>,
IRecipient<MailItemSelectionRemovedEvent>,
IRecipient<AccountSynchronizationCompleted>,
IRecipient<NewMailSynchronizationRequested>,
IRecipient<AccountSynchronizerStateChanged>,
@@ -57,10 +55,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly HashSet<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = [];
private IObservable<System.Reactive.EventPattern<NotifyCollectionChangedEventArgs>> selectionChangedObservable = null;
private ThrottledEventHandler _selectionChangedThrottler;
public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection();
public ObservableCollection<MailItemViewModel> SelectedItems { get; set; } = [];
//public ObservableCollection<MailItemViewModel> SelectedItems { get; set; } = [];
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
@@ -177,37 +175,56 @@ public partial class MailListPageViewModel : MailBaseViewModel,
mailListLength = statePersistenceService.MailListPaneLength;
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged));
selectionChangedObservable
.Throttle(TimeSpan.FromMilliseconds(100))
.Subscribe(async a =>
_selectionChangedThrottler = new ThrottledEventHandler(100, () =>
{
_ = ExecuteUIThread(() =>
{
await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); });
});
if (MailCollection.SelectedVisibleCount == 1)
{
ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0));
}
else
{
// At this point, either we don't have any item selected
// or we have multiple item selected. In either case
// there should be no active item.
//MailCollection.MailItemRemoved += (c, removedItem) =>
//{
// if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem)
// {
// foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>())
// {
// if (SelectedItems.Contains(viewModel))
// {
// SelectedItems.Remove(viewModel);
// }
// }
// }
// else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel))
// {
// SelectedItems.Remove(removedMailItemViewModel);
// }
//};
ActiveMailItemChanged(null);
}
NotifyItemSelected();
SetupTopBarActions();
});
});
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
MailCollection.SelectionChanged += SelectedItemsChanged;
}
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
base.OnNavigatedFrom(mode, parameters);
MailCollection.SelectionChanged -= SelectedItemsChanged;
MailCollection.Dispose();
_selectionChangedThrottler?.Dispose();
_selectionChangedThrottler = null;
}
private void SelectedItemsChanged(object sender, EventArgs e)
{
_selectionChangedThrottler?.Trigger();
}
private void SetupTopBarActions()
{
ActionItems.Clear();
var actions = GetAvailableMailActions(SelectedItems);
var actions = GetAvailableMailActions(MailCollection.SelectedVisibleItems);
actions.ForEach(a => ActionItems.Add(a));
}
@@ -248,13 +265,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
public int SelectedItemCount => SelectedItems.Count;
public bool HasMultipleItemSelections => SelectedItemCount > 1;
public bool HasSingleItemSelection => SelectedItemCount == 1;
public bool HasSelectedItems => SelectedItems.Any();
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
public string SelectedMessageText => HasSelectedItems ? string.Format(Translator.MailsSelected, SelectedItemCount) : Translator.NoMailSelected;
public string SelectedMessageText => MailCollection.SelectedVisibleCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedVisibleCount) : Translator.NoMailSelected;
/// <summary>
/// Indicates current state of the mail list. Doesn't matter it's loading or no.
@@ -333,13 +346,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public void NotifyItemSelected()
{
OnPropertyChanged(nameof(SelectedMessageText));
OnPropertyChanged(nameof(HasSingleItemSelection));
OnPropertyChanged(nameof(HasSelectedItems));
OnPropertyChanged(nameof(SelectedItemCount));
OnPropertyChanged(nameof(HasMultipleItemSelections));
//OnPropertyChanged(nameof(HasSingleItemSelection));
//OnPropertyChanged(nameof(HasSelectedItems));
//OnPropertyChanged(nameof(SelectedItemCount));
//OnPropertyChanged(nameof(HasMultipleItemSelections));
if (SelectedFolderPivot != null)
SelectedFolderPivot.SelectedItemCount = SelectedItemCount;
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
}
private void NotifyItemFoundState()
@@ -360,26 +372,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
});
}
private void SelectedItemCollectionUpdated(NotifyCollectionChangedEventArgs e)
{
if (SelectedItems.Count == 1)
{
ActiveMailItemChanged(SelectedItems[0]);
}
else
{
// At this point, either we don't have any item selected
// or we have multiple item selected. In either case
// there should be no active item.
ActiveMailItemChanged(null);
}
NotifyItemSelected();
SetupTopBarActions();
}
private async Task UpdateFolderPivotsAsync()
{
if (ActiveFolder == null) return;
@@ -440,9 +432,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem)
{
if (menuItem == null || !SelectedItems.Any()) return;
if (menuItem == null || MailCollection.SelectedVisibleCount == 0) return;
await HandleMailOperation(menuItem.Operation, SelectedItems);
await HandleMailOperation(menuItem.Operation, MailCollection.SelectedVisibleItems);
}
/// <summary>
@@ -452,9 +444,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
private async Task ExecuteMailOperation(MailOperation mailOperation)
{
if (!SelectedItems.Any()) return;
if (!MailCollection.SelectedVisibleItems.Any()) return;
await HandleMailOperation(mailOperation, SelectedItems);
await HandleMailOperation(mailOperation, MailCollection.SelectedVisibleItems);
}
private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable<MailItemViewModel> mailItems)
@@ -615,9 +607,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync();
// await MailCollection.AddAsync(addedMail);
await ExecuteUIThread(() => { NotifyItemFoundState(); });
await ExecuteUIThread(() =>
{
MailCollection.AddEmail(new MailItemViewModel(addedMail));
NotifyItemFoundState();
});
}
catch { }
finally
@@ -632,6 +626,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
// TODO
// await MailCollection.UpdateMailCopy(updatedMail);
await ExecuteUIThread(() => { SetupTopBarActions(); });
@@ -658,7 +653,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction)
{
bool isDeletedMailSelected = SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
bool isDeletedMailSelected = MailCollection.SelectedVisibleItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
// Automatically select the next item in the list if the setting is enabled.
MailItemViewModel nextItem = null;
@@ -667,12 +662,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{
await ExecuteUIThread(() =>
{
// nextItem = MailCollection.GetNextItem(removedMail);
nextItem = MailCollection.GetNextItem(removedMail);
});
}
// Remove the deleted item from the list.
// await MailCollection.RemoveAsync(removedMail);
await ExecuteUIThread(() =>
{
MailCollection.RemoveEmailByMailCopy(removedMail);
});
if (nextItem != null)
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true));
@@ -681,7 +679,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// There are no next item to select, but we removed the last item which was selected.
// Clearing selected item will dispose rendering page.
SelectedItems.Clear();
await ExecuteUIThread(() =>
{
MailCollection.ClearSelections();
});
}
await ExecuteUIThread(() => { NotifyItemFoundState(); });
@@ -743,8 +744,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{
MailCollection.Clear();
SelectedItems.Clear();
if (ActiveFolder == null)
return;
@@ -885,16 +884,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
#region Receivers
void IRecipient<MailItemSelectedEvent>.Receive(MailItemSelectedEvent message)
{
if (!SelectedItems.Contains(message.SelectedMailItem)) SelectedItems.Add(message.SelectedMailItem);
}
void IRecipient<MailItemSelectionRemovedEvent>.Receive(MailItemSelectionRemovedEvent message)
{
if (SelectedItems.Contains(message.RemovedMailItem)) SelectedItems.Remove(message.RemovedMailItem);
}
async void IRecipient<ActiveMailFolderChangedEvent>.Receive(ActiveMailFolderChangedEvent message)
{
NotifyItemSelected();
@@ -993,16 +982,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
for (int i = 0; i < 3; i++)
{
// TODO: Get container.
//var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
//if (mailContainer != null)
//{
// navigatingMailItem = mailContainer.ItemViewModel;
// threadMailItemViewModel = mailContainer.ThreadViewModel;
if (mailContainer != null)
{
navigatingMailItem = mailContainer.ItemViewModel;
threadMailItemViewModel = mailContainer.ThreadViewModel;
// break;
//}
break;
}
}
if (threadMailItemViewModel != null)
@@ -1079,7 +1067,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public void Receive(ThumbnailAdded message)
{
// MailCollection.UpdateThumbnails(message.Email);
Dispatcher.ExecuteOnUIThread(() =>
{
MailCollection.UpdateThumbnailsForAddress(message.Email);
});
}
protected override void RegisterRecipients()
@@ -1088,8 +1079,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Messenger.Register<MailItemNavigationRequested>(this);
Messenger.Register<ActiveMailFolderChangedEvent>(this);
Messenger.Register<MailItemSelectedEvent>(this);
Messenger.Register<MailItemSelectionRemovedEvent>(this);
Messenger.Register<AccountSynchronizationCompleted>(this);
Messenger.Register<NewMailSynchronizationRequested>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this);
@@ -1103,8 +1092,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Messenger.Unregister<MailItemNavigationRequested>(this);
Messenger.Unregister<ActiveMailFolderChangedEvent>(this);
Messenger.Unregister<MailItemSelectedEvent>(this);
Messenger.Unregister<MailItemSelectionRemovedEvent>(this);
Messenger.Unregister<AccountSynchronizationCompleted>(this);
Messenger.Unregister<NewMailSynchronizationRequested>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
@@ -1,18 +0,0 @@
using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Messages;
/// <summary>
/// Wino has complex selected item detection mechanism with nested ListViews that
/// supports multi selection with threads. Each list view will raise this for mail list page
/// to react.
/// </summary>
public class MailItemSelectedEvent
{
public MailItemSelectedEvent(MailItemViewModel selectedMailItem)
{
SelectedMailItem = selectedMailItem;
}
public MailItemViewModel SelectedMailItem { get; set; }
}
@@ -1,16 +0,0 @@
using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Messages;
/// <summary>
/// Selected item removed event.
/// </summary>
public class MailItemSelectionRemovedEvent
{
public MailItemSelectionRemovedEvent(MailItemViewModel removedMailItem)
{
RemovedMailItem = removedMailItem;
}
public MailItemViewModel RemovedMailItem { get; set; }
}
+2 -1
View File
@@ -104,10 +104,11 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
bool isStartupTaskLaunch = IsStartupTaskLaunch();
shellWindow.HandleAppActivation(args);
// Do not actiavate window if launched from startup task. Keep running in the system tray.
if (!isStartupTaskLaunch)
{
shellWindow.HandleAppActivation(args);
MainWindow.Activate();
}
}
@@ -6,8 +6,8 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
{
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
public string[] OutlookScope => new string[]
{
public string[] OutlookScope =>
[
"email",
"mail.readwrite",
"offline_access",
@@ -15,17 +15,17 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
"Mail.Send.Shared",
"Mail.ReadWrite.Shared",
"User.Read"
};
];
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
public string[] GmailScope => new string[]
{
public string[] GmailScope =>
[
"https://mail.google.com/",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/userinfo.email"
};
];
public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore";
}
+3 -6
View File
@@ -10,6 +10,7 @@ using EmailValidation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
using MimeKit;
using Windows.ApplicationModel.DataTransfer;
@@ -34,12 +35,10 @@ public sealed partial class ComposePage : ComposePageAbstract,
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
private readonly List<IDisposable> _disposables = [];
private readonly SystemNavigationManagerPreview _navManagerPreview = SystemNavigationManagerPreview.GetForCurrentView();
public ComposePage()
{
InitializeComponent();
_navManagerPreview.CloseRequested += OnClose;
}
private async void GlobalFocusManagerGotFocus(object sender, FocusManagerGotFocusEventArgs e)
@@ -235,9 +234,8 @@ public sealed partial class ComposePage : ComposePageAbstract,
FocusManager.GotFocus += GlobalFocusManagerGotFocus;
// TODO: disabled animation for now, since it's still not working properly.
//var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation");
//anim?.TryStart(GetWebView());
var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation");
anim?.TryStart(GetWebView());
_disposables.Add(GetSuggestionBoxDisposable(ToBox));
_disposables.Add(GetSuggestionBoxDisposable(CCBox));
@@ -371,7 +369,6 @@ public sealed partial class ComposePage : ComposePageAbstract,
base.OnNavigatingFrom(e);
FocusManager.GotFocus -= GlobalFocusManagerGotFocus;
_navManagerPreview.CloseRequested -= OnClose;
await ViewModel.UpdateMimeChangesAsync();
DisposeDisposables();
+9 -4
View File
@@ -33,6 +33,7 @@
mc:Ignorable="d">
<Page.Resources>
<x:Double x:Key="ItemContainerDisabledOpacity">1</x:Double>
<Thickness x:Key="ExpanderHeaderPadding">0,0,0,0</Thickness>
<Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness>
@@ -58,6 +59,7 @@
<ItemContainer
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
CanUserInvoke="UserCanInvoke"
IsSelected="{x:Bind IsSelected, Mode=TwoWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation To="1.0" Duration="0:0:1" />
@@ -129,12 +131,16 @@
</DataTemplate>
<DataTemplate x:Key="DateGroupHeaderTemplate" x:DataType="data:DateGroupHeader">
<ItemContainer CanUserSelect="UserCannotSelect" IsHitTestVisible="False">
<ItemContainer
CanUserSelect="UserCannotSelect"
IsHitTestVisible="False"
IsSelected="False">
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="1"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind DisplayName, Mode=OneWay}" />
</Grid>
@@ -251,7 +257,7 @@
<CommandBar
HorizontalAlignment="Left"
DefaultLabelPosition="Collapsed"
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.SelectedItemCount), Mode=OneWay}"
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedVisibleItems.Count), Mode=OneWay}"
OverflowButtonVisibility="Auto">
<interactivity:Interaction.Behaviors>
<local:BindableCommandBarBehavior ItemClickedCommand="{x:Bind ViewModel.ExecuteTopBarActionCommand}" PrimaryCommands="{x:Bind ViewModel.ActionItems, Mode=OneWay}" />
@@ -436,10 +442,9 @@
ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}"
Layout="{StaticResource DefaultItemsViewLayout}"
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
SelectionChanged="ListSelectionChanged"
ProcessKeyboardAccelerators="MailListView_ProcessKeyboardAccelerators"
SelectionMode="Extended" />
<!-- Try online search panel. -->
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}">
<Button
+44 -14
View File
@@ -481,7 +481,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
if (StatePersistenceService.IsReaderNarrowed)
{
if (ViewModel.HasSingleItemSelection && !isMultiSelectionEnabled)
if (ViewModel.MailCollection.HasSingleItemSelected && !isMultiSelectionEnabled)
{
VisualStateManager.GoToState(this, "NarrowRenderer", true);
}
@@ -492,7 +492,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
else
{
if (ViewModel.HasSingleItemSelection && !isMultiSelectionEnabled)
if (ViewModel.MailCollection.HasSingleItemSelected && !isMultiSelectionEnabled)
{
VisualStateManager.GoToState(this, "BothPanelsMailSelected", true);
}
@@ -505,6 +505,8 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
// MailListView.SelectAllWino();
}
@@ -554,19 +556,19 @@ public sealed partial class MailListPage : MailListPageAbstract,
private static object _selectedItemsLock = new object();
private void SynchronizeSelectedItems()
{
lock (_selectedItemsLock)
{
ViewModel.SelectedItems.Clear();
//lock (_selectedItemsLock)
//{
// ViewModel.SelectedItems.Clear();
foreach (var item in MailListView.SelectedItems)
{
if (item is MailItemViewModel mailItem)
{
if (!mailItem.IsSelected) mailItem.IsSelected = true;
if (!ViewModel.SelectedItems.Contains(mailItem)) ViewModel.SelectedItems.Add(mailItem);
}
}
}
// foreach (var item in MailListView.SelectedItems)
// {
// if (item is MailItemViewModel mailItem)
// {
// if (!mailItem.IsSelected) mailItem.IsSelected = true;
// if (!ViewModel.SelectedItems.Contains(mailItem)) ViewModel.SelectedItems.Add(mailItem);
// }
// }
//}
}
private void ThreadContainerRightTapped(object sender, RightTappedRoutedEventArgs e)
@@ -596,4 +598,32 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
}
}
private void MailListView_ProcessKeyboardAccelerators(UIElement sender, ProcessKeyboardAcceleratorEventArgs args)
{
// ItemsView have weird logic for selection inversion. We need to handle it manually.
args.Handled = true;
ViewModel.MailCollection.SelectAll();
// If not all items are selected, select all. Otherwise clear selection.
// Handle selections in the GroupedEmailCollection.
//if (MailListView.SelectedItems.Count < MailListView.CastedItemsSource?.Count())
//{
// // MailListView.SelectAllWino();
//}
//else
//{
// // MailListView.ClearSelections();
//}
}
private void SingleItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
{
if (args.InvokedItem is MailItemViewModel mailItem)
{
mailItem.IsSelected = !mailItem.IsSelected;
}
}
}