New WinoListView implementation with multiple selections.
This commit is contained in:
@@ -1180,7 +1180,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
|
||||
foreach (var expander in _threadExpanders.Values)
|
||||
{
|
||||
expander.IsSelected = false;
|
||||
|
||||
|
||||
// Also explicitly deselect individual emails within threads
|
||||
foreach (var threadEmail in expander.ThreadEmails)
|
||||
{
|
||||
@@ -1363,7 +1363,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
|
||||
return item switch
|
||||
{
|
||||
MailItemViewModel email => email.MailCopy?.CreationDate ?? DateTime.MinValue,
|
||||
ThreadMailItemViewModel expander => expander.LatestEmailDate,
|
||||
ThreadMailItemViewModel expander => expander.LatestMailViewModel?.CreationDate ?? DateTime.MinValue,
|
||||
_ => DateTime.MinValue
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,15 +3,19 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MoreLinq.Extensions;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections;
|
||||
|
||||
public class WinoMailCollection
|
||||
public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsChangedMessage>
|
||||
{
|
||||
// We cache each mail copy id for faster access on updates.
|
||||
// If the item provider here for update or removal doesn't exist here
|
||||
@@ -20,6 +24,7 @@ public class WinoMailCollection
|
||||
public HashSet<Guid> MailCopyIdHashSet = [];
|
||||
|
||||
public event EventHandler<MailItemViewModel> MailItemRemoved;
|
||||
public event EventHandler ItemSelectionChanged;
|
||||
|
||||
private ListItemComparer listComparer = new();
|
||||
|
||||
@@ -32,6 +37,16 @@ public class WinoMailCollection
|
||||
/// </summary>
|
||||
public SortingOptionType SortingType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the grouping type for emails.
|
||||
/// Note: WinoMailCollection groups automatically on the UI, so this just affects the grouping key logic.
|
||||
/// </summary>
|
||||
public EmailGroupingType GroupingType
|
||||
{
|
||||
get => SortingType == SortingOptionType.ReceiveDate ? EmailGroupingType.ByDate : EmailGroupingType.ByFromName;
|
||||
set => SortingType = value == EmailGroupingType.ByDate ? SortingOptionType.ReceiveDate : SortingOptionType.Sender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically deletes single mail items after the delete operation or thread->single transition.
|
||||
/// This is useful when reply draft is discarded in the thread. Only enabled for Draft folder for now.
|
||||
@@ -40,14 +55,31 @@ public class WinoMailCollection
|
||||
|
||||
public int Count => _mailItemSource.Count;
|
||||
|
||||
public bool IsAllSelected
|
||||
{
|
||||
get
|
||||
{
|
||||
return AllItemsCount == SelectedItemsCount;
|
||||
}
|
||||
}
|
||||
|
||||
public IDispatcher CoreDispatcher { get; set; }
|
||||
|
||||
public WinoMailCollection()
|
||||
{
|
||||
MailItems = new ReadOnlyObservableGroupedCollection<object, IMailListItem>(_mailItemSource);
|
||||
|
||||
Messenger.Register<SelectedItemsChangedMessage>(this);
|
||||
}
|
||||
|
||||
public void Clear() => _mailItemSource.Clear();
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_mailItemSource.Clear();
|
||||
MailCopyIdHashSet.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
private object GetGroupingKey(IMailListItem mailItem)
|
||||
{
|
||||
@@ -72,13 +104,16 @@ public class WinoMailCollection
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertItemInternal(object groupKey, IMailListItem mailItem)
|
||||
private async Task InsertItemInternalAsync(object groupKey, IMailListItem mailItem)
|
||||
{
|
||||
UpdateUniqueIdHashes(mailItem, true);
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer);
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer);
|
||||
});
|
||||
}
|
||||
|
||||
private void RemoveItemInternal(ObservableGroup<object, IMailListItem> group, IMailListItem mailItem)
|
||||
private async Task RemoveItemInternalAsync(ObservableGroup<object, IMailListItem> group, IMailListItem mailItem)
|
||||
{
|
||||
UpdateUniqueIdHashes(mailItem, false);
|
||||
|
||||
@@ -94,12 +129,15 @@ public class WinoMailCollection
|
||||
}
|
||||
}
|
||||
|
||||
group.Remove(mailItem);
|
||||
|
||||
if (group.Count == 0)
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_mailItemSource.RemoveGroup(group.Key);
|
||||
}
|
||||
group.Remove(mailItem);
|
||||
|
||||
if (group.Count == 0)
|
||||
{
|
||||
_mailItemSource.RemoveGroup(group.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleThreadingAsync(ObservableGroup<object, IMailListItem> group, IMailListItem item, MailCopy addedItem)
|
||||
@@ -118,8 +156,8 @@ public class WinoMailCollection
|
||||
{
|
||||
var existingGroupKey = GetGroupingKey(threadViewModel);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
var newMailItem = new MailItemViewModel(addedItem);
|
||||
threadViewModel.AddEmail(newMailItem);
|
||||
});
|
||||
@@ -152,17 +190,14 @@ public class WinoMailCollection
|
||||
|
||||
private async Task MoveThreadToNewGroupAsync(ObservableGroup<object, IMailListItem> currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
RemoveItemInternal(currentGroup, threadViewModel);
|
||||
InsertItemInternal(newGroupKey, threadViewModel);
|
||||
});
|
||||
await RemoveItemInternalAsync(currentGroup, threadViewModel);
|
||||
await InsertItemInternalAsync(newGroupKey, threadViewModel);
|
||||
}
|
||||
|
||||
private async Task CreateNewThreadAsync(ObservableGroup<object, IMailListItem> group, MailItemViewModel item, MailCopy addedItem)
|
||||
{
|
||||
var threadViewModel = new ThreadMailItemViewModel(item.MailCopy.ThreadId);
|
||||
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
threadViewModel.AddEmail(item);
|
||||
@@ -171,11 +206,8 @@ public class WinoMailCollection
|
||||
|
||||
var newGroupKey = GetGroupingKey(threadViewModel);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
RemoveItemInternal(group, item);
|
||||
InsertItemInternal(newGroupKey, threadViewModel);
|
||||
});
|
||||
await RemoveItemInternalAsync(group, item);
|
||||
await InsertItemInternalAsync(newGroupKey, threadViewModel);
|
||||
}
|
||||
|
||||
public async Task AddAsync(MailCopy addedItem)
|
||||
@@ -185,8 +217,8 @@ public class WinoMailCollection
|
||||
foreach (var item in group)
|
||||
{
|
||||
// Compare ThreadIds - if they match and both have ThreadIds, thread them together
|
||||
bool shouldThread = !string.IsNullOrEmpty(addedItem.ThreadId) &&
|
||||
item is MailItemViewModel mailItem &&
|
||||
bool shouldThread = !string.IsNullOrEmpty(addedItem.ThreadId) &&
|
||||
item is MailItemViewModel mailItem &&
|
||||
!string.IsNullOrEmpty(mailItem.MailCopy.ThreadId) &&
|
||||
string.Equals(addedItem.ThreadId, mailItem.MailCopy.ThreadId, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -194,7 +226,7 @@ public class WinoMailCollection
|
||||
{
|
||||
// Check if any email in the thread has matching ThreadId
|
||||
shouldThread = !string.IsNullOrEmpty(addedItem.ThreadId) &&
|
||||
threadViewModel.ThreadEmails.Any(e =>
|
||||
threadViewModel.ThreadEmails.Any(e =>
|
||||
!string.IsNullOrEmpty(e.MailCopy.ThreadId) &&
|
||||
string.Equals(addedItem.ThreadId, e.MailCopy.ThreadId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
@@ -219,7 +251,7 @@ public class WinoMailCollection
|
||||
{
|
||||
var newMailItem = new MailItemViewModel(addedItem);
|
||||
var groupKey = GetGroupingKey(newMailItem);
|
||||
await ExecuteUIThread(() => { InsertItemInternal(groupKey, newMailItem); });
|
||||
await InsertItemInternalAsync(groupKey, newMailItem);
|
||||
}
|
||||
|
||||
private async Task UpdateExistingItemAsync(MailItemViewModel existingItem, MailCopy updatedItem)
|
||||
@@ -230,7 +262,10 @@ public class WinoMailCollection
|
||||
await ExecuteUIThread(() => { existingItem.MailCopy = updatedItem; });
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<IMailListItem> items, bool clearIdCache)
|
||||
/// <summary>
|
||||
/// Adds multiple emails to the collection.
|
||||
/// </summary>
|
||||
public async Task AddRangeAsync(IEnumerable<IMailListItem> items, bool clearIdCache)
|
||||
{
|
||||
if (clearIdCache)
|
||||
{
|
||||
@@ -238,31 +273,34 @@ public class WinoMailCollection
|
||||
}
|
||||
|
||||
var groupedByName = items
|
||||
.GroupBy(a => GetGroupingKey(a))
|
||||
.GroupBy(GetGroupingKey)
|
||||
.Select(a => new ObservableGroup<object, IMailListItem>(a.Key, a));
|
||||
|
||||
foreach (var group in groupedByName)
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
// Store all mail copy ids for faster access.
|
||||
foreach (var item in group)
|
||||
{
|
||||
UpdateUniqueIdHashes(item, true);
|
||||
}
|
||||
|
||||
var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key);
|
||||
|
||||
if (existingGroup == null)
|
||||
{
|
||||
_mailItemSource.AddGroup(group.Key, group);
|
||||
}
|
||||
else
|
||||
foreach (var group in groupedByName)
|
||||
{
|
||||
// Store all mail copy ids for faster access.
|
||||
foreach (var item in group)
|
||||
{
|
||||
existingGroup.Add(item);
|
||||
UpdateUniqueIdHashes(item, true);
|
||||
}
|
||||
|
||||
var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key);
|
||||
|
||||
if (existingGroup == null)
|
||||
{
|
||||
_mailItemSource.AddGroup(group.Key, group);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in group)
|
||||
{
|
||||
existingGroup.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public MailItemContainer GetMailItemContainer(Guid uniqueMailId)
|
||||
@@ -291,17 +329,20 @@ public class WinoMailCollection
|
||||
return null;
|
||||
}
|
||||
|
||||
public void UpdateThumbnails(string address)
|
||||
/// <summary>
|
||||
/// Updates thumbnails for all mail items with the specified address.
|
||||
/// </summary>
|
||||
public Task UpdateThumbnailsForAddressAsync(string address)
|
||||
{
|
||||
if (CoreDispatcher == null) return;
|
||||
if (CoreDispatcher == null) return Task.CompletedTask;
|
||||
|
||||
CoreDispatcher.ExecuteOnUIThread(() =>
|
||||
return CoreDispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
foreach (var group in _mailItemSource)
|
||||
{
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase))
|
||||
if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress?.Equals(address, StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent;
|
||||
}
|
||||
@@ -309,7 +350,7 @@ public class WinoMailCollection
|
||||
{
|
||||
foreach (var threadMailItem in threadViewModel.ThreadEmails)
|
||||
{
|
||||
if (threadMailItem.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase))
|
||||
if (threadMailItem.MailCopy.FromAddress?.Equals(address, StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
threadMailItem.ThumbnailUpdatedEvent = !threadMailItem.ThumbnailUpdatedEvent;
|
||||
}
|
||||
@@ -325,15 +366,12 @@ public class WinoMailCollection
|
||||
/// </summary>
|
||||
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateMailCopy(MailCopy updatedMailCopy)
|
||||
public Task UpdateMailCopy(MailCopy updatedMailCopy)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId)) return Task.CompletedTask;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
return ExecuteUIThread(() =>
|
||||
{
|
||||
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
|
||||
|
||||
@@ -356,6 +394,8 @@ public class WinoMailCollection
|
||||
});
|
||||
}
|
||||
|
||||
public MailItemViewModel GetFirst() => AllItems.ElementAtOrDefault(0);
|
||||
|
||||
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
||||
{
|
||||
try
|
||||
@@ -466,11 +506,8 @@ public class WinoMailCollection
|
||||
var singleViewModel = threadMailItemViewModel.ThreadEmails.First();
|
||||
var groupKey = GetGroupingKey(singleViewModel);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
RemoveItemInternal(group, threadMailItemViewModel);
|
||||
InsertItemInternal(groupKey, singleViewModel);
|
||||
});
|
||||
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||
await InsertItemInternalAsync(groupKey, singleViewModel);
|
||||
|
||||
// If thread->single conversion is being done, we should ignore it for non-draft items.
|
||||
// eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added.
|
||||
@@ -483,13 +520,13 @@ public class WinoMailCollection
|
||||
|
||||
if (newGroup != null)
|
||||
{
|
||||
await ExecuteUIThread(() => { RemoveItemInternal(newGroup, singleViewModel); });
|
||||
await RemoveItemInternalAsync(newGroup, singleViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (threadMailItemViewModel.EmailCount == 0)
|
||||
{
|
||||
await ExecuteUIThread(() => { RemoveItemInternal(group, threadMailItemViewModel); });
|
||||
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -502,7 +539,7 @@ public class WinoMailCollection
|
||||
}
|
||||
else if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.UniqueId == removeItem.UniqueId)
|
||||
{
|
||||
await ExecuteUIThread(() => { RemoveItemInternal(group, item); });
|
||||
await RemoveItemInternalAsync(group, item);
|
||||
|
||||
shouldExit = true;
|
||||
|
||||
@@ -510,7 +547,120 @@ public class WinoMailCollection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await NotifySelectionChangesAsync();
|
||||
}
|
||||
|
||||
private IEnumerable<MailItemViewModel> AllItems
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var group in _mailItemSource)
|
||||
{
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (item is not MailItemViewModel mailItemViewModel) throw new Exception("Item is not MailItemViewModel in AllItems");
|
||||
|
||||
if (item is ThreadMailItemViewModel threadMail)
|
||||
{
|
||||
foreach (var singleItem in threadMail.ThreadEmails)
|
||||
{
|
||||
yield return singleItem;
|
||||
}
|
||||
}
|
||||
|
||||
yield return mailItemViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<MailItemViewModel> SelectedItems => AllItems.Where(a => a.IsSelected);
|
||||
public int SelectedItemsCount => AllItems.Count(a => a.IsSelected);
|
||||
public int AllItemsCount => AllItems.Count();
|
||||
public bool IsAllItemsSelected => AllItems.Any() && AllItems.All(a => a.IsSelected);
|
||||
public bool HasSingleItemSelected => SelectedItemsCount == 1;
|
||||
|
||||
public async Task ExecuteWithoutRaiseSelectionChangedAsync(Action<MailItemViewModel> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Do not listen to individual selection changes while we are doing bulk selection.
|
||||
Messenger.Unregister<SelectedItemsChangedMessage>(this);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
foreach (var item in AllItems)
|
||||
{
|
||||
action(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
Messenger.Register<SelectedItemsChangedMessage>(this);
|
||||
Messenger.Send(new SelectedItemsChangedMessage());
|
||||
|
||||
await NotifySelectionChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public Task ToggleSelectAllAsync()
|
||||
{
|
||||
if (IsAllItemsSelected)
|
||||
{
|
||||
return UnselectAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
return SelectAllAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of an item in the flat Items collection.
|
||||
/// Note: WinoMailCollection doesn't have a flat Items collection like GroupedEmailCollection.
|
||||
/// This returns -1 as it's not applicable to the grouped structure.
|
||||
/// </summary>
|
||||
public int IndexOf(object item)
|
||||
{
|
||||
// WinoMailCollection uses grouped structure, so we need to search through groups
|
||||
int currentIndex = 0;
|
||||
|
||||
foreach (var group in _mailItemSource)
|
||||
{
|
||||
foreach (var groupItem in group)
|
||||
{
|
||||
if (ReferenceEquals(groupItem, item))
|
||||
{
|
||||
return currentIndex;
|
||||
}
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Task SelectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = true);
|
||||
public Task UnselectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = false);
|
||||
|
||||
private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action);
|
||||
|
||||
public void Receive(SelectedItemsChangedMessage message) => _ = NotifySelectionChangesAsync();
|
||||
|
||||
private async Task NotifySelectionChangesAsync()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsAllItemsSelected));
|
||||
OnPropertyChanged(nameof(SelectedItemsCount));
|
||||
OnPropertyChanged(nameof(HasSingleItemSelected));
|
||||
|
||||
ItemSelectionChanged?.Invoke(this, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
@@ -7,7 +9,7 @@ namespace Wino.Mail.ViewModels.Data;
|
||||
/// Common interface for mail items that can be displayed in a mail list.
|
||||
/// Implemented by both MailItemViewModel and ThreadMailItemViewModel.
|
||||
/// </summary>
|
||||
public interface IMailListItem : IMailHashContainer
|
||||
public interface IMailListItem : IMailHashContainer, INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the latest creation date for sorting purposes.
|
||||
@@ -22,4 +24,18 @@ public interface IMailListItem : IMailHashContainer
|
||||
/// For ThreadMailItemViewModel: the latest email's from name
|
||||
/// </summary>
|
||||
string FromName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this item is selected.
|
||||
/// For MailItemViewModel: returns IsSelected
|
||||
/// For ThreadMailItemViewModel: returns IsSelected
|
||||
/// </summary>
|
||||
bool IsSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all selected mail items within this list item.
|
||||
/// For MailItemViewModel: returns itself if IsSelected is true, otherwise empty
|
||||
/// For ThreadMailItemViewModel: returns all selected emails within the thread
|
||||
/// </summary>
|
||||
IEnumerable<MailItemViewModel> GetSelectedMailItems();
|
||||
}
|
||||
|
||||
@@ -91,4 +91,12 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => [MailCopy.UniqueId];
|
||||
|
||||
public IEnumerable<MailItemViewModel> GetSelectedMailItems()
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
yield return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,4 +118,18 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable,
|
||||
public bool HasUniqueId(Guid uniqueId) => _threadEmails.Any(email => email.MailCopy.UniqueId == uniqueId);
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => ThreadEmails.Select(a => a.MailCopy.UniqueId);
|
||||
|
||||
public IEnumerable<MailItemViewModel> GetSelectedMailItems()
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
// If the thread itself is selected, return all emails in the thread
|
||||
return ThreadEmails;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, return only individually selected emails within the thread
|
||||
return ThreadEmails.Where(e => e.IsSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ 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;
|
||||
@@ -55,11 +54,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
private readonly HashSet<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = [];
|
||||
|
||||
private ThrottledEventHandler _selectionChangedThrottler;
|
||||
|
||||
public event EventHandler ThrottledSelectionChanged;
|
||||
public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection();
|
||||
//public ObservableCollection<MailItemViewModel> SelectedItems { get; set; } = [];
|
||||
public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection();
|
||||
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
|
||||
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
|
||||
|
||||
@@ -121,7 +116,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
private string barMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private double mailListLength = 420;
|
||||
public partial double MailListLength { get; set; } = 420;
|
||||
|
||||
[ObservableProperty]
|
||||
private double maxMailListLength = 1200;
|
||||
@@ -158,11 +153,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
INewThemeService themeService,
|
||||
IWinoLogger winoLogger)
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
ThemeService = themeService;
|
||||
_winoLogger = winoLogger;
|
||||
StatePersistenceService = statePersistenceService;
|
||||
NavigationService = navigationService;
|
||||
_accountService = accountService;
|
||||
_mailDialogService = mailDialogService;
|
||||
_mailService = mailService;
|
||||
@@ -171,63 +162,77 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_keyPressService = keyPressService;
|
||||
|
||||
PreferencesService = preferencesService;
|
||||
ThemeService = themeService;
|
||||
StatePersistenceService = statePersistenceService;
|
||||
NavigationService = navigationService;
|
||||
|
||||
SelectedFilterOption = FilterOptions[0];
|
||||
SelectedSortingOption = SortingOptions[0];
|
||||
|
||||
mailListLength = statePersistenceService.MailListPaneLength;
|
||||
MailListLength = statePersistenceService.MailListPaneLength;
|
||||
|
||||
_selectionChangedThrottler = new ThrottledEventHandler(100, () =>
|
||||
{
|
||||
_ = ExecuteUIThread(() =>
|
||||
{
|
||||
if (MailCollection.SelectedVisibleCount == 1)
|
||||
{
|
||||
ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
// At this point, either we don't have any item selected
|
||||
// or we have multiple item selected. In either case
|
||||
// there should be no active item.
|
||||
//_selectionChangedThrottler = new ThrottledEventHandler(100, () =>
|
||||
//{
|
||||
// _ = ExecuteUIThread(() =>
|
||||
// {
|
||||
// if (MailCollection.SelectedVisibleCount == 1)
|
||||
// {
|
||||
// ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // At this point, either we don't have any item selected
|
||||
// // or we have multiple item selected. In either case
|
||||
// // there should be no active item.
|
||||
|
||||
ActiveMailItemChanged(null);
|
||||
}
|
||||
// ActiveMailItemChanged(null);
|
||||
// }
|
||||
|
||||
NotifyItemSelected();
|
||||
SetupTopBarActions();
|
||||
});
|
||||
// NotifyItemSelected();
|
||||
// SetupTopBarActions();
|
||||
// });
|
||||
|
||||
ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
});
|
||||
// ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
//});
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
MailCollection.SelectionChanged += SelectedItemsChanged;
|
||||
MailCollection.ItemSelectionChanged += MailItemSelectionChanged;
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
MailCollection.SelectionChanged -= SelectedItemsChanged;
|
||||
MailCollection.Dispose();
|
||||
|
||||
_selectionChangedThrottler?.Dispose();
|
||||
_selectionChangedThrottler = null;
|
||||
MailCollection.ItemSelectionChanged -= MailItemSelectionChanged;
|
||||
}
|
||||
|
||||
private void SelectedItemsChanged(object sender, EventArgs e)
|
||||
private void MailItemSelectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
_selectionChangedThrottler?.Trigger();
|
||||
if (MailCollection.HasSingleItemSelected)
|
||||
{
|
||||
var selectedItem = MailCollection.SelectedItems.ElementAtOrDefault(0);
|
||||
ActiveMailItemChanged(selectedItem);
|
||||
}
|
||||
else if (MailCollection.SelectedItemsCount > 1)
|
||||
{
|
||||
ActiveMailItemChanged(null);
|
||||
}
|
||||
|
||||
NotifyItemFoundState();
|
||||
NotifyItemSelected();
|
||||
SetupTopBarActions();
|
||||
}
|
||||
|
||||
private void SetupTopBarActions()
|
||||
{
|
||||
ActionItems.Clear();
|
||||
var actions = GetAvailableMailActions(MailCollection.SelectedVisibleItems);
|
||||
|
||||
var actions = GetAvailableMailActions(MailCollection.SelectedItems);
|
||||
actions.ForEach(a => ActionItems.Add(a));
|
||||
}
|
||||
|
||||
@@ -270,12 +275,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
||||
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
||||
|
||||
public string SelectedMessageText => MailCollection.SelectedVisibleCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedVisibleCount) : Translator.NoMailSelected;
|
||||
public string SelectedMessageText => MailCollection.SelectedItemsCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedItemsCount) : Translator.NoMailSelected;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates current state of the mail list. Doesn't matter it's loading or no.
|
||||
/// </summary>
|
||||
public bool IsEmpty => MailCollection.Count == 0;
|
||||
public bool IsEmpty => MailCollection.AllItemsCount == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Progress ring only should be visible when the folder is initializing and there are no items. We don't need to show it when there are items.
|
||||
@@ -349,10 +354,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
public void NotifyItemSelected()
|
||||
{
|
||||
OnPropertyChanged(nameof(SelectedMessageText));
|
||||
//OnPropertyChanged(nameof(HasSingleItemSelection));
|
||||
//OnPropertyChanged(nameof(HasSelectedItems));
|
||||
//OnPropertyChanged(nameof(SelectedItemCount));
|
||||
//OnPropertyChanged(nameof(HasMultipleItemSelections));
|
||||
|
||||
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
|
||||
}
|
||||
@@ -435,9 +436,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
[RelayCommand]
|
||||
private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem)
|
||||
{
|
||||
if (menuItem == null || MailCollection.SelectedVisibleCount == 0) return;
|
||||
if (menuItem == null || MailCollection.SelectedItemsCount == 0) return;
|
||||
|
||||
await HandleMailOperation(menuItem.Operation, MailCollection.SelectedVisibleItems);
|
||||
await HandleMailOperation(menuItem.Operation, MailCollection.SelectedItems);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -447,9 +448,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
[RelayCommand]
|
||||
private async Task ExecuteMailOperation(MailOperation mailOperation)
|
||||
{
|
||||
if (!MailCollection.SelectedVisibleItems.Any()) return;
|
||||
if (MailCollection.SelectedItemsCount == 0) return;
|
||||
|
||||
await HandleMailOperation(mailOperation, MailCollection.SelectedVisibleItems);
|
||||
await HandleMailOperation(mailOperation, MailCollection.SelectedItems);
|
||||
}
|
||||
|
||||
private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable<MailItemViewModel> mailItems)
|
||||
@@ -564,7 +565,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
var viewModels = PrepareMailViewModels(items);
|
||||
|
||||
await ExecuteUIThread(() => { MailCollection.AddEmails(viewModels); });
|
||||
await MailCollection.AddRangeAsync(viewModels, false);
|
||||
await ExecuteUIThread(() => { IsInitializingFolder = false; });
|
||||
}
|
||||
|
||||
@@ -585,6 +586,14 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
return condition;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void RemoveFirst()
|
||||
{
|
||||
var fi = MailCollection.GetFirst();
|
||||
|
||||
Messenger.Send(new MailRemovedMessage(fi.MailCopy));
|
||||
}
|
||||
|
||||
protected override async void OnMailAdded(MailCopy addedMail)
|
||||
{
|
||||
base.OnMailAdded(addedMail);
|
||||
@@ -610,9 +619,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
MailCollection.AddEmail(new MailItemViewModel(addedMail));
|
||||
await MailCollection.AddAsync(addedMail);
|
||||
NotifyItemFoundState();
|
||||
});
|
||||
}
|
||||
@@ -629,8 +638,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
|
||||
|
||||
// TODO
|
||||
// await MailCollection.UpdateMailCopy(updatedMail);
|
||||
await MailCollection.UpdateMailCopy(updatedMail);
|
||||
|
||||
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
||||
}
|
||||
@@ -656,7 +664,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction)
|
||||
{
|
||||
bool isDeletedMailSelected = MailCollection.SelectedVisibleItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
|
||||
bool isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
|
||||
|
||||
// Automatically select the next item in the list if the setting is enabled.
|
||||
MailItemViewModel nextItem = null;
|
||||
@@ -672,7 +680,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
// Remove the deleted item from the list.
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MailCollection.RemoveEmailByMailCopy(removedMail);
|
||||
_ = MailCollection.RemoveAsync(removedMail);
|
||||
});
|
||||
|
||||
if (nextItem != null)
|
||||
@@ -684,7 +692,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MailCollection.ClearSelections();
|
||||
_ = MailCollection.UnselectAllAsync();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -707,10 +715,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
// Otherwise the draft mail item will be duplicated on the next add execution.
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
// Create the item. Draft folder navigation is already done at this point.
|
||||
MailCollection.AddEmail(new MailItemViewModel(draftMail));
|
||||
await MailCollection.AddAsync(draftMail);
|
||||
|
||||
// New draft is created by user. Select the item.
|
||||
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
|
||||
@@ -745,7 +753,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
try
|
||||
{
|
||||
MailCollection.Clear();
|
||||
_ = MailCollection.ClearAsync();
|
||||
|
||||
if (ActiveFolder == null)
|
||||
return;
|
||||
@@ -847,10 +855,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
var viewModels = PrepareMailViewModels(items);
|
||||
|
||||
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MailCollection.AddEmails(viewModels);
|
||||
|
||||
if (isDoingSearch && !isDoingOnlineSearch)
|
||||
{
|
||||
IsOnlineSearchButtonVisible = true;
|
||||
@@ -1059,21 +1067,25 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
if (handlingFolder == null) return;
|
||||
|
||||
_ = ExecuteUIThread(() =>
|
||||
_ = ExecuteUIThread(async () =>
|
||||
{
|
||||
MailCollection.Clear();
|
||||
await MailCollection.ClearAsync();
|
||||
|
||||
_mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
|
||||
MailCollection.CoreDispatcher = Dispatcher;
|
||||
}
|
||||
|
||||
public void Receive(ThumbnailAdded message)
|
||||
{
|
||||
Dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
MailCollection.UpdateThumbnailsForAddress(message.Email);
|
||||
});
|
||||
_ = MailCollection.UpdateThumbnailsForAddressAsync(message.Email);
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
|
||||
@@ -19,4 +19,7 @@
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user