Fixing some issues with ItemsView and selections.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user