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>(); var requests = new List<IMailActionRequest>();
// TODO: Fix: Collection was modified; enumeration operation may not execute // 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); var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution);
@@ -6,6 +6,7 @@ using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages; using CommunityToolkit.Mvvm.Messaging.Messages;
using Wino.Core.Domain.Entities.Mail;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Collections; namespace Wino.Mail.ViewModels.Collections;
@@ -34,12 +35,15 @@ public enum EmailSortDirection
/// </summary> /// </summary>
public partial class GroupedEmailCollection : ObservableObject, IRecipient<PropertyChangedMessage<bool>>, IDisposable public partial class GroupedEmailCollection : ObservableObject, IRecipient<PropertyChangedMessage<bool>>, IDisposable
{ {
public event EventHandler SelectionChanged;
private readonly ObservableCollection<MailItemViewModel> _sourceItems = []; private readonly ObservableCollection<MailItemViewModel> _sourceItems = [];
private readonly Dictionary<string, GroupHeaderBase> _groupHeaders = []; private readonly Dictionary<string, GroupHeaderBase> _groupHeaders = [];
private readonly Dictionary<string, int> _groupHeaderIndexCache = []; private readonly Dictionary<string, int> _groupHeaderIndexCache = [];
private readonly Dictionary<string, List<object>> _groupItems = []; private readonly Dictionary<string, List<object>> _groupItems = [];
private readonly Dictionary<string, ThreadMailItemViewModel> _threadExpanders = []; private readonly Dictionary<string, ThreadMailItemViewModel> _threadExpanders = [];
private readonly HashSet<Guid> _mailCopyIdHashSet = []; private readonly HashSet<Guid> _mailCopyIdHashSet = [];
private readonly HashSet<MailItemViewModel> _selectedVisibleItems = [];
private bool _disposed; private bool _disposed;
private bool _isUpdating; private bool _isUpdating;
@@ -49,6 +53,28 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
[ObservableProperty] [ObservableProperty]
private EmailSortDirection sortDirection = EmailSortDirection.Descending; 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() public GroupedEmailCollection()
{ {
// Create a flat collection for ItemsView with headers, expanders and emails mixed // Create a flat collection for ItemsView with headers, expanders and emails mixed
@@ -89,6 +115,12 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
/// </summary> /// </summary>
public IEnumerable<MailItemViewModel> AllItems => _sourceItems; 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> /// <summary>
/// Gets all currently selected email items. /// Gets all currently selected email items.
/// Includes: /// Includes:
@@ -163,7 +195,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
} }
/// <summary> /// <summary>
/// Handles PropertyChanged messages for thread expansion /// Handles PropertyChanged messages for thread expansion and mail item selection
/// </summary> /// </summary>
public void Receive(PropertyChangedMessage<bool> message) public void Receive(PropertyChangedMessage<bool> message)
{ {
@@ -175,6 +207,94 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
{ {
HandleThreadExpansion(expander); 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) private void HandleThreadExpansion(ThreadMailItemViewModel expander)
@@ -197,6 +317,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var email in sortedThreadEmails) foreach (var email in sortedThreadEmails)
{ {
Items.Insert(insertIndex, email); Items.Insert(insertIndex, email);
RegisterMailItemForSelectionTracking(email);
insertIndex++; insertIndex++;
} }
@@ -210,8 +331,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var emailIndex = Items.IndexOf(email); var emailIndex = Items.IndexOf(email);
if (emailIndex >= 0) if (emailIndex >= 0)
{ {
var sourceItem = _sourceItems.FirstOrDefault(a => a == email); UnregisterMailItemFromSelectionTracking(email);
Items.RemoveAt(emailIndex); Items.RemoveAt(emailIndex);
UpdateHeaderIndicesAfterRemoval(emailIndex); UpdateHeaderIndicesAfterRemoval(emailIndex);
} }
@@ -315,17 +435,35 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
if (email?.MailCopy == null) if (email?.MailCopy == null)
return; 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 // Remove from unique ID tracking
_mailCopyIdHashSet.Remove(email.MailCopy.UniqueId); _mailCopyIdHashSet.Remove(mailCopy.UniqueId);
_isUpdating = true; _isUpdating = true;
try 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 // Remove from source collection
if (!_sourceItems.Remove(email)) _sourceItems.Remove(email);
return; // Email not found
if (!string.IsNullOrEmpty(threadId) && _threadExpanders.TryGetValue(threadId, out var expander)) if (!string.IsNullOrEmpty(threadId) && _threadExpanders.TryGetValue(threadId, out var expander))
{ {
@@ -333,7 +471,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
expander.RemoveEmail(email); expander.RemoveEmail(email);
email.IsDisplayedInThread = false; email.IsDisplayedInThread = false;
// Remove email from UI // Remove email from UI if it's visible (only if thread is expanded)
RemoveEmailFromUI(email); RemoveEmailFromUI(email);
// Check if thread now has only 1 email - convert back to standalone // 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; _isUpdating = true;
try 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 // Reset IsDisplayedInThread for all emails before clearing
foreach (var email in _sourceItems) foreach (var email in _sourceItems)
{ {
@@ -453,6 +605,12 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
OnPropertyChanged(nameof(TotalCount)); OnPropertyChanged(nameof(TotalCount));
OnPropertyChanged(nameof(TotalUnreadCount)); OnPropertyChanged(nameof(TotalUnreadCount));
OnPropertyChanged(nameof(SelectedVisibleItems));
if (hadSelectedItems)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
} }
finally finally
{ {
@@ -481,14 +639,32 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
_isUpdating = true; _isUpdating = true;
try 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 // Clear UI items but preserve source and expanders
Items.Clear(); Items.Clear();
_groupHeaders.Clear(); _groupHeaders.Clear();
_groupHeaderIndexCache.Clear(); _groupHeaderIndexCache.Clear();
_groupItems.Clear(); _groupItems.Clear();
var hadSelectedItems = _selectedVisibleItems.Count > 0;
_selectedVisibleItems.Clear();
SelectedVisibleCount = 0;
if (!_sourceItems.Any()) if (!_sourceItems.Any())
{
if (hadSelectedItems)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
return; return;
}
// Rebuild thread expanders based on current emails // Rebuild thread expanders based on current emails
RebuildThreadExpanders(); RebuildThreadExpanders();
@@ -550,6 +726,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var threadEmail in sortedThreadEmails) foreach (var threadEmail in sortedThreadEmails)
{ {
Items.Add(threadEmail); Items.Add(threadEmail);
RegisterMailItemForSelectionTracking(threadEmail);
currentIndex++; currentIndex++;
} }
} }
@@ -558,6 +735,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
{ {
// Add standalone email // Add standalone email
Items.Add(email); Items.Add(email);
RegisterMailItemForSelectionTracking(email);
currentIndex++; currentIndex++;
} }
} }
@@ -565,6 +743,11 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
// Update group header counts // Update group header counts
UpdateAllGroupHeaderCounts(); UpdateAllGroupHeaderCounts();
if (hadSelectedItems)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
} }
finally finally
{ {
@@ -604,6 +787,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
// Insert the email at the correct position // Insert the email at the correct position
Items.Insert(insertPosition, email); Items.Insert(insertPosition, email);
RegisterMailItemForSelectionTracking(email);
// Update the group items list // Update the group items list
if (!_groupItems.ContainsKey(groupKey)) if (!_groupItems.ContainsKey(groupKey))
@@ -762,6 +946,196 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
return null; 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) private void AddThreadToUI(ThreadMailItemViewModel expander, string groupKey)
{ {
var groupHeader = GetOrCreateGroupHeader(groupKey); var groupHeader = GetOrCreateGroupHeader(groupKey);
@@ -789,6 +1163,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var email in sortedThreadEmails) foreach (var email in sortedThreadEmails)
{ {
Items.Insert(currentIndex, email); Items.Insert(currentIndex, email);
RegisterMailItemForSelectionTracking(email);
currentIndex++; currentIndex++;
totalInserted++; totalInserted++;
} }
@@ -819,6 +1194,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
foreach (var email in sortedThreadEmails) foreach (var email in sortedThreadEmails)
{ {
Items.Insert(currentIndex, email); Items.Insert(currentIndex, email);
RegisterMailItemForSelectionTracking(email);
currentIndex++; currentIndex++;
totalInserted++; totalInserted++;
} }
@@ -846,6 +1222,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var emailIndex = Items.IndexOf(email); var emailIndex = Items.IndexOf(email);
if (emailIndex >= 0) if (emailIndex >= 0)
{ {
UnregisterMailItemFromSelectionTracking(email);
Items.RemoveAt(emailIndex); Items.RemoveAt(emailIndex);
UpdateHeaderIndicesAfterRemoval(emailIndex); UpdateHeaderIndicesAfterRemoval(emailIndex);
} }
@@ -857,6 +1234,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var itemIndex = Items.IndexOf(email); var itemIndex = Items.IndexOf(email);
if (itemIndex >= 0) if (itemIndex >= 0)
{ {
UnregisterMailItemFromSelectionTracking(email);
Items.RemoveAt(itemIndex); Items.RemoveAt(itemIndex);
UpdateHeaderIndicesAfterRemoval(itemIndex); UpdateHeaderIndicesAfterRemoval(itemIndex);
} }
@@ -874,6 +1252,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var insertPosition = FindGroupInsertionPosition(groupKey); var insertPosition = FindGroupInsertionPosition(groupKey);
Items.Insert(insertPosition, groupHeader); Items.Insert(insertPosition, groupHeader);
Items.Insert(insertPosition + 1, email); Items.Insert(insertPosition + 1, email);
RegisterMailItemForSelectionTracking(email);
UpdateHeaderIndicesAfterInsertion(insertPosition, 2); UpdateHeaderIndicesAfterInsertion(insertPosition, 2);
_groupHeaderIndexCache[groupKey] = insertPosition; _groupHeaderIndexCache[groupKey] = insertPosition;
@@ -884,6 +1263,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
var groupEndIndex = FindGroupEndIndex(headerIndex); var groupEndIndex = FindGroupEndIndex(headerIndex);
var insertIndex = FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex); var insertIndex = FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex);
Items.Insert(insertIndex, email); Items.Insert(insertIndex, email);
RegisterMailItemForSelectionTracking(email);
UpdateHeaderIndicesAfterInsertion(insertIndex); UpdateHeaderIndicesAfterInsertion(insertIndex);
} }
@@ -1321,6 +1701,15 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
{ {
_sourceItems.CollectionChanged -= OnSourceItemsChanged; _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 // Unregister from messenger
WeakReferenceMessenger.Default.Unregister<PropertyChangedMessage<bool>>(this); WeakReferenceMessenger.Default.Unregister<PropertyChangedMessage<bool>>(this);
@@ -1342,6 +1731,8 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
_groupHeaderIndexCache.Clear(); _groupHeaderIndexCache.Clear();
_groupItems.Clear(); _groupItems.Clear();
_threadExpanders.Clear(); _threadExpanders.Clear();
_selectedVisibleItems.Clear();
SelectedVisibleCount = 0;
} }
_disposed = true; _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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -21,11 +19,13 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Collections;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Helpers;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server; using Wino.Messaging.Server;
@@ -36,8 +36,6 @@ namespace Wino.Mail.ViewModels;
public partial class MailListPageViewModel : MailBaseViewModel, public partial class MailListPageViewModel : MailBaseViewModel,
IRecipient<MailItemNavigationRequested>, IRecipient<MailItemNavigationRequested>,
IRecipient<ActiveMailFolderChangedEvent>, IRecipient<ActiveMailFolderChangedEvent>,
IRecipient<MailItemSelectedEvent>,
IRecipient<MailItemSelectionRemovedEvent>,
IRecipient<AccountSynchronizationCompleted>, IRecipient<AccountSynchronizationCompleted>,
IRecipient<NewMailSynchronizationRequested>, IRecipient<NewMailSynchronizationRequested>,
IRecipient<AccountSynchronizerStateChanged>, IRecipient<AccountSynchronizerStateChanged>,
@@ -57,10 +55,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly HashSet<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = []; private readonly HashSet<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = [];
private IObservable<System.Reactive.EventPattern<NotifyCollectionChangedEventArgs>> selectionChangedObservable = null; private ThrottledEventHandler _selectionChangedThrottler;
public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection(); 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<FolderPivotViewModel> PivotFolders { get; set; } = [];
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = []; public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
@@ -177,37 +175,56 @@ public partial class MailListPageViewModel : MailBaseViewModel,
mailListLength = statePersistenceService.MailListPaneLength; mailListLength = statePersistenceService.MailListPaneLength;
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged)); _selectionChangedThrottler = new ThrottledEventHandler(100, () =>
selectionChangedObservable {
.Throttle(TimeSpan.FromMilliseconds(100)) _ = ExecuteUIThread(() =>
.Subscribe(async a =>
{ {
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) => ActiveMailItemChanged(null);
//{ }
// if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem)
// { NotifyItemSelected();
// foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>()) SetupTopBarActions();
// { });
// if (SelectedItems.Contains(viewModel)) });
// { }
// SelectedItems.Remove(viewModel);
// } public override void OnNavigatedTo(NavigationMode mode, object parameters)
// } {
// } base.OnNavigatedTo(mode, parameters);
// else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel))
// { MailCollection.SelectionChanged += SelectedItemsChanged;
// SelectedItems.Remove(removedMailItemViewModel); }
// }
//}; 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() private void SetupTopBarActions()
{ {
ActionItems.Clear(); ActionItems.Clear();
var actions = GetAvailableMailActions(SelectedItems); var actions = GetAvailableMailActions(MailCollection.SelectedVisibleItems);
actions.ForEach(a => ActionItems.Add(a)); actions.ForEach(a => ActionItems.Add(a));
} }
@@ -248,13 +265,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled; public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false; 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 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> /// <summary>
/// Indicates current state of the mail list. Doesn't matter it's loading or no. /// 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() public void NotifyItemSelected()
{ {
OnPropertyChanged(nameof(SelectedMessageText)); OnPropertyChanged(nameof(SelectedMessageText));
OnPropertyChanged(nameof(HasSingleItemSelection)); //OnPropertyChanged(nameof(HasSingleItemSelection));
OnPropertyChanged(nameof(HasSelectedItems)); //OnPropertyChanged(nameof(HasSelectedItems));
OnPropertyChanged(nameof(SelectedItemCount)); //OnPropertyChanged(nameof(SelectedItemCount));
OnPropertyChanged(nameof(HasMultipleItemSelections)); //OnPropertyChanged(nameof(HasMultipleItemSelections));
if (SelectedFolderPivot != null) SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
SelectedFolderPivot.SelectedItemCount = SelectedItemCount;
} }
private void NotifyItemFoundState() 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() private async Task UpdateFolderPivotsAsync()
{ {
if (ActiveFolder == null) return; if (ActiveFolder == null) return;
@@ -440,9 +432,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand] [RelayCommand]
private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem) 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> /// <summary>
@@ -452,9 +444,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand] [RelayCommand]
private async Task ExecuteMailOperation(MailOperation mailOperation) 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) private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable<MailItemViewModel> mailItems)
@@ -615,9 +607,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync(); await listManipulationSemepahore.WaitAsync();
// await MailCollection.AddAsync(addedMail); await ExecuteUIThread(() =>
{
await ExecuteUIThread(() => { NotifyItemFoundState(); }); MailCollection.AddEmail(new MailItemViewModel(addedMail));
NotifyItemFoundState();
});
} }
catch { } catch { }
finally finally
@@ -632,6 +626,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
// TODO
// await MailCollection.UpdateMailCopy(updatedMail); // await MailCollection.UpdateMailCopy(updatedMail);
await ExecuteUIThread(() => { SetupTopBarActions(); }); await ExecuteUIThread(() => { SetupTopBarActions(); });
@@ -658,7 +653,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction) 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. // Automatically select the next item in the list if the setting is enabled.
MailItemViewModel nextItem = null; MailItemViewModel nextItem = null;
@@ -667,12 +662,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{ {
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
// nextItem = MailCollection.GetNextItem(removedMail); nextItem = MailCollection.GetNextItem(removedMail);
}); });
} }
// Remove the deleted item from the list. // Remove the deleted item from the list.
// await MailCollection.RemoveAsync(removedMail); await ExecuteUIThread(() =>
{
MailCollection.RemoveEmailByMailCopy(removedMail);
});
if (nextItem != null) if (nextItem != null)
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true)); 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. // There are no next item to select, but we removed the last item which was selected.
// Clearing selected item will dispose rendering page. // Clearing selected item will dispose rendering page.
SelectedItems.Clear(); await ExecuteUIThread(() =>
{
MailCollection.ClearSelections();
});
} }
await ExecuteUIThread(() => { NotifyItemFoundState(); }); await ExecuteUIThread(() => { NotifyItemFoundState(); });
@@ -743,8 +744,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{ {
MailCollection.Clear(); MailCollection.Clear();
SelectedItems.Clear();
if (ActiveFolder == null) if (ActiveFolder == null)
return; return;
@@ -885,16 +884,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
#region Receivers #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) async void IRecipient<ActiveMailFolderChangedEvent>.Receive(ActiveMailFolderChangedEvent message)
{ {
NotifyItemSelected(); NotifyItemSelected();
@@ -993,16 +982,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
for (int i = 0; i < 3; i++) 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) if (mailContainer != null)
//{ {
// navigatingMailItem = mailContainer.ItemViewModel; navigatingMailItem = mailContainer.ItemViewModel;
// threadMailItemViewModel = mailContainer.ThreadViewModel; threadMailItemViewModel = mailContainer.ThreadViewModel;
// break; break;
//} }
} }
if (threadMailItemViewModel != null) if (threadMailItemViewModel != null)
@@ -1079,7 +1067,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public void Receive(ThumbnailAdded message) public void Receive(ThumbnailAdded message)
{ {
// MailCollection.UpdateThumbnails(message.Email); Dispatcher.ExecuteOnUIThread(() =>
{
MailCollection.UpdateThumbnailsForAddress(message.Email);
});
} }
protected override void RegisterRecipients() protected override void RegisterRecipients()
@@ -1088,8 +1079,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Messenger.Register<MailItemNavigationRequested>(this); Messenger.Register<MailItemNavigationRequested>(this);
Messenger.Register<ActiveMailFolderChangedEvent>(this); Messenger.Register<ActiveMailFolderChangedEvent>(this);
Messenger.Register<MailItemSelectedEvent>(this);
Messenger.Register<MailItemSelectionRemovedEvent>(this);
Messenger.Register<AccountSynchronizationCompleted>(this); Messenger.Register<AccountSynchronizationCompleted>(this);
Messenger.Register<NewMailSynchronizationRequested>(this); Messenger.Register<NewMailSynchronizationRequested>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this); Messenger.Register<AccountSynchronizerStateChanged>(this);
@@ -1103,8 +1092,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Messenger.Unregister<MailItemNavigationRequested>(this); Messenger.Unregister<MailItemNavigationRequested>(this);
Messenger.Unregister<ActiveMailFolderChangedEvent>(this); Messenger.Unregister<ActiveMailFolderChangedEvent>(this);
Messenger.Unregister<MailItemSelectedEvent>(this);
Messenger.Unregister<MailItemSelectionRemovedEvent>(this);
Messenger.Unregister<AccountSynchronizationCompleted>(this); Messenger.Unregister<AccountSynchronizationCompleted>(this);
Messenger.Unregister<NewMailSynchronizationRequested>(this); Messenger.Unregister<NewMailSynchronizationRequested>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(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(); bool isStartupTaskLaunch = IsStartupTaskLaunch();
shellWindow.HandleAppActivation(args);
// Do not actiavate window if launched from startup task. Keep running in the system tray. // Do not actiavate window if launched from startup task. Keep running in the system tray.
if (!isStartupTaskLaunch) if (!isStartupTaskLaunch)
{ {
shellWindow.HandleAppActivation(args);
MainWindow.Activate(); MainWindow.Activate();
} }
} }
@@ -6,8 +6,8 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
{ {
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208"; public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
public string[] OutlookScope => new string[] public string[] OutlookScope =>
{ [
"email", "email",
"mail.readwrite", "mail.readwrite",
"offline_access", "offline_access",
@@ -15,17 +15,17 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
"Mail.Send.Shared", "Mail.Send.Shared",
"Mail.ReadWrite.Shared", "Mail.ReadWrite.Shared",
"User.Read" "User.Read"
}; ];
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
public string[] GmailScope => new string[] public string[] GmailScope =>
{ [
"https://mail.google.com/", "https://mail.google.com/",
"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/gmail.labels", "https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/userinfo.email" "https://www.googleapis.com/auth/userinfo.email"
}; ];
public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore"; public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore";
} }
+3 -6
View File
@@ -10,6 +10,7 @@ using EmailValidation;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using MimeKit; using MimeKit;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
@@ -34,12 +35,10 @@ public sealed partial class ComposePage : ComposePageAbstract,
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView(); public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
private readonly List<IDisposable> _disposables = []; private readonly List<IDisposable> _disposables = [];
private readonly SystemNavigationManagerPreview _navManagerPreview = SystemNavigationManagerPreview.GetForCurrentView();
public ComposePage() public ComposePage()
{ {
InitializeComponent(); InitializeComponent();
_navManagerPreview.CloseRequested += OnClose;
} }
private async void GlobalFocusManagerGotFocus(object sender, FocusManagerGotFocusEventArgs e) private async void GlobalFocusManagerGotFocus(object sender, FocusManagerGotFocusEventArgs e)
@@ -235,9 +234,8 @@ public sealed partial class ComposePage : ComposePageAbstract,
FocusManager.GotFocus += GlobalFocusManagerGotFocus; FocusManager.GotFocus += GlobalFocusManagerGotFocus;
// TODO: disabled animation for now, since it's still not working properly. var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation");
//var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation"); anim?.TryStart(GetWebView());
//anim?.TryStart(GetWebView());
_disposables.Add(GetSuggestionBoxDisposable(ToBox)); _disposables.Add(GetSuggestionBoxDisposable(ToBox));
_disposables.Add(GetSuggestionBoxDisposable(CCBox)); _disposables.Add(GetSuggestionBoxDisposable(CCBox));
@@ -371,7 +369,6 @@ public sealed partial class ComposePage : ComposePageAbstract,
base.OnNavigatingFrom(e); base.OnNavigatingFrom(e);
FocusManager.GotFocus -= GlobalFocusManagerGotFocus; FocusManager.GotFocus -= GlobalFocusManagerGotFocus;
_navManagerPreview.CloseRequested -= OnClose;
await ViewModel.UpdateMimeChangesAsync(); await ViewModel.UpdateMimeChangesAsync();
DisposeDisposables(); DisposeDisposables();
+9 -4
View File
@@ -33,6 +33,7 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Page.Resources> <Page.Resources>
<x:Double x:Key="ItemContainerDisabledOpacity">1</x:Double>
<Thickness x:Key="ExpanderHeaderPadding">0,0,0,0</Thickness> <Thickness x:Key="ExpanderHeaderPadding">0,0,0,0</Thickness>
<Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness> <Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness>
@@ -58,6 +59,7 @@
<ItemContainer <ItemContainer
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
CanUserInvoke="UserCanInvoke"
IsSelected="{x:Bind IsSelected, Mode=TwoWay}"> IsSelected="{x:Bind IsSelected, Mode=TwoWay}">
<animations:Implicit.ShowAnimations> <animations:Implicit.ShowAnimations>
<animations:OpacityAnimation To="1.0" Duration="0:0:1" /> <animations:OpacityAnimation To="1.0" Duration="0:0:1" />
@@ -129,12 +131,16 @@
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="DateGroupHeaderTemplate" x:DataType="data:DateGroupHeader"> <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}"> <Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
<TextBlock <TextBlock
FontSize="14" FontSize="14"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}" Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="1"
Style="{ThemeResource CaptionTextBlockStyle}" Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind DisplayName, Mode=OneWay}" /> Text="{x:Bind DisplayName, Mode=OneWay}" />
</Grid> </Grid>
@@ -251,7 +257,7 @@
<CommandBar <CommandBar
HorizontalAlignment="Left" HorizontalAlignment="Left"
DefaultLabelPosition="Collapsed" 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"> OverflowButtonVisibility="Auto">
<interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
<local:BindableCommandBarBehavior ItemClickedCommand="{x:Bind ViewModel.ExecuteTopBarActionCommand}" PrimaryCommands="{x:Bind ViewModel.ActionItems, Mode=OneWay}" /> <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}" ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}"
Layout="{StaticResource DefaultItemsViewLayout}" Layout="{StaticResource DefaultItemsViewLayout}"
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}" LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
SelectionChanged="ListSelectionChanged" ProcessKeyboardAccelerators="MailListView_ProcessKeyboardAccelerators"
SelectionMode="Extended" /> SelectionMode="Extended" />
<!-- Try online search panel. --> <!-- Try online search panel. -->
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}"> <Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}">
<Button <Button
+44 -14
View File
@@ -481,7 +481,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
if (StatePersistenceService.IsReaderNarrowed) if (StatePersistenceService.IsReaderNarrowed)
{ {
if (ViewModel.HasSingleItemSelection && !isMultiSelectionEnabled) if (ViewModel.MailCollection.HasSingleItemSelected && !isMultiSelectionEnabled)
{ {
VisualStateManager.GoToState(this, "NarrowRenderer", true); VisualStateManager.GoToState(this, "NarrowRenderer", true);
} }
@@ -492,7 +492,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
else else
{ {
if (ViewModel.HasSingleItemSelection && !isMultiSelectionEnabled) if (ViewModel.MailCollection.HasSingleItemSelected && !isMultiSelectionEnabled)
{ {
VisualStateManager.GoToState(this, "BothPanelsMailSelected", true); VisualStateManager.GoToState(this, "BothPanelsMailSelected", true);
} }
@@ -505,6 +505,8 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{ {
// MailListView.SelectAllWino(); // MailListView.SelectAllWino();
} }
@@ -554,19 +556,19 @@ public sealed partial class MailListPage : MailListPageAbstract,
private static object _selectedItemsLock = new object(); private static object _selectedItemsLock = new object();
private void SynchronizeSelectedItems() private void SynchronizeSelectedItems()
{ {
lock (_selectedItemsLock) //lock (_selectedItemsLock)
{ //{
ViewModel.SelectedItems.Clear(); // ViewModel.SelectedItems.Clear();
foreach (var item in MailListView.SelectedItems) // foreach (var item in MailListView.SelectedItems)
{ // {
if (item is MailItemViewModel mailItem) // if (item is MailItemViewModel mailItem)
{ // {
if (!mailItem.IsSelected) mailItem.IsSelected = true; // if (!mailItem.IsSelected) mailItem.IsSelected = true;
if (!ViewModel.SelectedItems.Contains(mailItem)) ViewModel.SelectedItems.Add(mailItem); // if (!ViewModel.SelectedItems.Contains(mailItem)) ViewModel.SelectedItems.Add(mailItem);
} // }
} // }
} //}
} }
private void ThreadContainerRightTapped(object sender, RightTappedRoutedEventArgs e) 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;
}
}
} }