New WinoListView implementation with multiple selections.
This commit is contained in:
@@ -1363,7 +1363,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient<Prope
|
|||||||
return item switch
|
return item switch
|
||||||
{
|
{
|
||||||
MailItemViewModel email => email.MailCopy?.CreationDate ?? DateTime.MinValue,
|
MailItemViewModel email => email.MailCopy?.CreationDate ?? DateTime.MinValue,
|
||||||
ThreadMailItemViewModel expander => expander.LatestEmailDate,
|
ThreadMailItemViewModel expander => expander.LatestMailViewModel?.CreationDate ?? DateTime.MinValue,
|
||||||
_ => DateTime.MinValue
|
_ => DateTime.MinValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Collections;
|
using CommunityToolkit.Mvvm.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using MoreLinq.Extensions;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
using Wino.Messaging.Client.Mails;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels.Collections;
|
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.
|
// We cache each mail copy id for faster access on updates.
|
||||||
// If the item provider here for update or removal doesn't exist here
|
// 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 HashSet<Guid> MailCopyIdHashSet = [];
|
||||||
|
|
||||||
public event EventHandler<MailItemViewModel> MailItemRemoved;
|
public event EventHandler<MailItemViewModel> MailItemRemoved;
|
||||||
|
public event EventHandler ItemSelectionChanged;
|
||||||
|
|
||||||
private ListItemComparer listComparer = new();
|
private ListItemComparer listComparer = new();
|
||||||
|
|
||||||
@@ -32,6 +37,16 @@ public class WinoMailCollection
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SortingOptionType SortingType { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Automatically deletes single mail items after the delete operation or thread->single transition.
|
/// 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.
|
/// 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 int Count => _mailItemSource.Count;
|
||||||
|
|
||||||
|
public bool IsAllSelected
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return AllItemsCount == SelectedItemsCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IDispatcher CoreDispatcher { get; set; }
|
public IDispatcher CoreDispatcher { get; set; }
|
||||||
|
|
||||||
public WinoMailCollection()
|
public WinoMailCollection()
|
||||||
{
|
{
|
||||||
MailItems = new ReadOnlyObservableGroupedCollection<object, IMailListItem>(_mailItemSource);
|
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)
|
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);
|
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);
|
UpdateUniqueIdHashes(mailItem, false);
|
||||||
|
|
||||||
@@ -94,12 +129,15 @@ public class WinoMailCollection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
group.Remove(mailItem);
|
await ExecuteUIThread(() =>
|
||||||
|
|
||||||
if (group.Count == 0)
|
|
||||||
{
|
{
|
||||||
_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)
|
private async Task HandleThreadingAsync(ObservableGroup<object, IMailListItem> group, IMailListItem item, MailCopy addedItem)
|
||||||
@@ -152,11 +190,8 @@ public class WinoMailCollection
|
|||||||
|
|
||||||
private async Task MoveThreadToNewGroupAsync(ObservableGroup<object, IMailListItem> currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey)
|
private async Task MoveThreadToNewGroupAsync(ObservableGroup<object, IMailListItem> currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey)
|
||||||
{
|
{
|
||||||
await ExecuteUIThread(() =>
|
await RemoveItemInternalAsync(currentGroup, threadViewModel);
|
||||||
{
|
await InsertItemInternalAsync(newGroupKey, threadViewModel);
|
||||||
RemoveItemInternal(currentGroup, threadViewModel);
|
|
||||||
InsertItemInternal(newGroupKey, threadViewModel);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateNewThreadAsync(ObservableGroup<object, IMailListItem> group, MailItemViewModel item, MailCopy addedItem)
|
private async Task CreateNewThreadAsync(ObservableGroup<object, IMailListItem> group, MailItemViewModel item, MailCopy addedItem)
|
||||||
@@ -171,11 +206,8 @@ public class WinoMailCollection
|
|||||||
|
|
||||||
var newGroupKey = GetGroupingKey(threadViewModel);
|
var newGroupKey = GetGroupingKey(threadViewModel);
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await RemoveItemInternalAsync(group, item);
|
||||||
{
|
await InsertItemInternalAsync(newGroupKey, threadViewModel);
|
||||||
RemoveItemInternal(group, item);
|
|
||||||
InsertItemInternal(newGroupKey, threadViewModel);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddAsync(MailCopy addedItem)
|
public async Task AddAsync(MailCopy addedItem)
|
||||||
@@ -219,7 +251,7 @@ public class WinoMailCollection
|
|||||||
{
|
{
|
||||||
var newMailItem = new MailItemViewModel(addedItem);
|
var newMailItem = new MailItemViewModel(addedItem);
|
||||||
var groupKey = GetGroupingKey(newMailItem);
|
var groupKey = GetGroupingKey(newMailItem);
|
||||||
await ExecuteUIThread(() => { InsertItemInternal(groupKey, newMailItem); });
|
await InsertItemInternalAsync(groupKey, newMailItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateExistingItemAsync(MailItemViewModel existingItem, MailCopy updatedItem)
|
private async Task UpdateExistingItemAsync(MailItemViewModel existingItem, MailCopy updatedItem)
|
||||||
@@ -230,7 +262,10 @@ public class WinoMailCollection
|
|||||||
await ExecuteUIThread(() => { existingItem.MailCopy = updatedItem; });
|
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)
|
if (clearIdCache)
|
||||||
{
|
{
|
||||||
@@ -238,31 +273,34 @@ public class WinoMailCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
var groupedByName = items
|
var groupedByName = items
|
||||||
.GroupBy(a => GetGroupingKey(a))
|
.GroupBy(GetGroupingKey)
|
||||||
.Select(a => new ObservableGroup<object, IMailListItem>(a.Key, a));
|
.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 group in groupedByName)
|
||||||
foreach (var item in group)
|
|
||||||
{
|
|
||||||
UpdateUniqueIdHashes(item, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key);
|
|
||||||
|
|
||||||
if (existingGroup == null)
|
|
||||||
{
|
|
||||||
_mailItemSource.AddGroup(group.Key, group);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
// Store all mail copy ids for faster access.
|
||||||
foreach (var item in group)
|
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)
|
public MailItemContainer GetMailItemContainer(Guid uniqueMailId)
|
||||||
@@ -291,17 +329,20 @@ public class WinoMailCollection
|
|||||||
return null;
|
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 group in _mailItemSource)
|
||||||
{
|
{
|
||||||
foreach (var item in group)
|
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;
|
mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent;
|
||||||
}
|
}
|
||||||
@@ -309,7 +350,7 @@ public class WinoMailCollection
|
|||||||
{
|
{
|
||||||
foreach (var threadMailItem in threadViewModel.ThreadEmails)
|
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;
|
threadMailItem.ThumbnailUpdatedEvent = !threadMailItem.ThumbnailUpdatedEvent;
|
||||||
}
|
}
|
||||||
@@ -325,15 +366,12 @@ public class WinoMailCollection
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task UpdateMailCopy(MailCopy updatedMailCopy)
|
public Task UpdateMailCopy(MailCopy updatedMailCopy)
|
||||||
{
|
{
|
||||||
// This item doesn't exist in the list.
|
// This item doesn't exist in the list.
|
||||||
if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId))
|
if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId)) return Task.CompletedTask;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
return ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
|
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
|
||||||
|
|
||||||
@@ -356,6 +394,8 @@ public class WinoMailCollection
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MailItemViewModel GetFirst() => AllItems.ElementAtOrDefault(0);
|
||||||
|
|
||||||
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -466,11 +506,8 @@ public class WinoMailCollection
|
|||||||
var singleViewModel = threadMailItemViewModel.ThreadEmails.First();
|
var singleViewModel = threadMailItemViewModel.ThreadEmails.First();
|
||||||
var groupKey = GetGroupingKey(singleViewModel);
|
var groupKey = GetGroupingKey(singleViewModel);
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||||
{
|
await InsertItemInternalAsync(groupKey, singleViewModel);
|
||||||
RemoveItemInternal(group, threadMailItemViewModel);
|
|
||||||
InsertItemInternal(groupKey, singleViewModel);
|
|
||||||
});
|
|
||||||
|
|
||||||
// If thread->single conversion is being done, we should ignore it for non-draft items.
|
// 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.
|
// 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)
|
if (newGroup != null)
|
||||||
{
|
{
|
||||||
await ExecuteUIThread(() => { RemoveItemInternal(newGroup, singleViewModel); });
|
await RemoveItemInternalAsync(newGroup, singleViewModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (threadMailItemViewModel.EmailCount == 0)
|
else if (threadMailItemViewModel.EmailCount == 0)
|
||||||
{
|
{
|
||||||
await ExecuteUIThread(() => { RemoveItemInternal(group, threadMailItemViewModel); });
|
await RemoveItemInternalAsync(group, threadMailItemViewModel);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -502,7 +539,7 @@ public class WinoMailCollection
|
|||||||
}
|
}
|
||||||
else if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.UniqueId == removeItem.UniqueId)
|
else if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.UniqueId == removeItem.UniqueId)
|
||||||
{
|
{
|
||||||
await ExecuteUIThread(() => { RemoveItemInternal(group, item); });
|
await RemoveItemInternalAsync(group, item);
|
||||||
|
|
||||||
shouldExit = true;
|
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);
|
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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels.Data;
|
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.
|
/// Common interface for mail items that can be displayed in a mail list.
|
||||||
/// Implemented by both MailItemViewModel and ThreadMailItemViewModel.
|
/// Implemented by both MailItemViewModel and ThreadMailItemViewModel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMailListItem : IMailHashContainer
|
public interface IMailListItem : IMailHashContainer, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the latest creation date for sorting purposes.
|
/// Gets the latest creation date for sorting purposes.
|
||||||
@@ -22,4 +24,18 @@ public interface IMailListItem : IMailHashContainer
|
|||||||
/// For ThreadMailItemViewModel: the latest email's from name
|
/// For ThreadMailItemViewModel: the latest email's from name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string FromName { get; }
|
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<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 bool HasUniqueId(Guid uniqueId) => _threadEmails.Any(email => email.MailCopy.UniqueId == uniqueId);
|
||||||
|
|
||||||
public IEnumerable<Guid> GetContainingIds() => ThreadEmails.Select(a => a.MailCopy.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.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;
|
||||||
@@ -55,11 +54,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
private readonly HashSet<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = [];
|
private readonly HashSet<Guid> gmailUnreadFolderMarkedAsReadUniqueIds = [];
|
||||||
|
|
||||||
private ThrottledEventHandler _selectionChangedThrottler;
|
public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection();
|
||||||
|
|
||||||
public event EventHandler ThrottledSelectionChanged;
|
|
||||||
public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection();
|
|
||||||
//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; } = [];
|
||||||
|
|
||||||
@@ -121,7 +116,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
private string barMessage;
|
private string barMessage;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double mailListLength = 420;
|
public partial double MailListLength { get; set; } = 420;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double maxMailListLength = 1200;
|
private double maxMailListLength = 1200;
|
||||||
@@ -158,11 +153,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
INewThemeService themeService,
|
INewThemeService themeService,
|
||||||
IWinoLogger winoLogger)
|
IWinoLogger winoLogger)
|
||||||
{
|
{
|
||||||
PreferencesService = preferencesService;
|
|
||||||
ThemeService = themeService;
|
|
||||||
_winoLogger = winoLogger;
|
_winoLogger = winoLogger;
|
||||||
StatePersistenceService = statePersistenceService;
|
|
||||||
NavigationService = navigationService;
|
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_mailDialogService = mailDialogService;
|
_mailDialogService = mailDialogService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
@@ -171,63 +162,77 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
_winoRequestDelegator = winoRequestDelegator;
|
_winoRequestDelegator = winoRequestDelegator;
|
||||||
_keyPressService = keyPressService;
|
_keyPressService = keyPressService;
|
||||||
|
|
||||||
|
PreferencesService = preferencesService;
|
||||||
|
ThemeService = themeService;
|
||||||
|
StatePersistenceService = statePersistenceService;
|
||||||
|
NavigationService = navigationService;
|
||||||
|
|
||||||
SelectedFilterOption = FilterOptions[0];
|
SelectedFilterOption = FilterOptions[0];
|
||||||
SelectedSortingOption = SortingOptions[0];
|
SelectedSortingOption = SortingOptions[0];
|
||||||
|
|
||||||
mailListLength = statePersistenceService.MailListPaneLength;
|
MailListLength = statePersistenceService.MailListPaneLength;
|
||||||
|
|
||||||
_selectionChangedThrottler = new ThrottledEventHandler(100, () =>
|
//_selectionChangedThrottler = new ThrottledEventHandler(100, () =>
|
||||||
{
|
//{
|
||||||
_ = ExecuteUIThread(() =>
|
// _ = ExecuteUIThread(() =>
|
||||||
{
|
// {
|
||||||
if (MailCollection.SelectedVisibleCount == 1)
|
// if (MailCollection.SelectedVisibleCount == 1)
|
||||||
{
|
// {
|
||||||
ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0));
|
// ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0));
|
||||||
}
|
// }
|
||||||
else
|
// else
|
||||||
{
|
// {
|
||||||
// At this point, either we don't have any item selected
|
// // At this point, either we don't have any item selected
|
||||||
// or we have multiple item selected. In either case
|
// // or we have multiple item selected. In either case
|
||||||
// there should be no active item.
|
// // there should be no active item.
|
||||||
|
|
||||||
ActiveMailItemChanged(null);
|
// ActiveMailItemChanged(null);
|
||||||
}
|
// }
|
||||||
|
|
||||||
NotifyItemSelected();
|
// NotifyItemSelected();
|
||||||
SetupTopBarActions();
|
// SetupTopBarActions();
|
||||||
});
|
// });
|
||||||
|
|
||||||
ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty);
|
// ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty);
|
||||||
});
|
//});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedTo(mode, parameters);
|
base.OnNavigatedTo(mode, parameters);
|
||||||
|
|
||||||
MailCollection.SelectionChanged += SelectedItemsChanged;
|
MailCollection.ItemSelectionChanged += MailItemSelectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||||
{
|
{
|
||||||
base.OnNavigatedFrom(mode, parameters);
|
base.OnNavigatedFrom(mode, parameters);
|
||||||
|
|
||||||
MailCollection.SelectionChanged -= SelectedItemsChanged;
|
MailCollection.ItemSelectionChanged -= MailItemSelectionChanged;
|
||||||
MailCollection.Dispose();
|
|
||||||
|
|
||||||
_selectionChangedThrottler?.Dispose();
|
|
||||||
_selectionChangedThrottler = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
private void SetupTopBarActions()
|
||||||
{
|
{
|
||||||
ActionItems.Clear();
|
ActionItems.Clear();
|
||||||
var actions = GetAvailableMailActions(MailCollection.SelectedVisibleItems);
|
|
||||||
|
var actions = GetAvailableMailActions(MailCollection.SelectedItems);
|
||||||
actions.ForEach(a => ActionItems.Add(a));
|
actions.ForEach(a => ActionItems.Add(a));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,12 +275,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
||||||
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
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>
|
/// <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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsEmpty => MailCollection.Count == 0;
|
public bool IsEmpty => MailCollection.AllItemsCount == 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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()
|
public void NotifyItemSelected()
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(SelectedMessageText));
|
OnPropertyChanged(nameof(SelectedMessageText));
|
||||||
//OnPropertyChanged(nameof(HasSingleItemSelection));
|
|
||||||
//OnPropertyChanged(nameof(HasSelectedItems));
|
|
||||||
//OnPropertyChanged(nameof(SelectedItemCount));
|
|
||||||
//OnPropertyChanged(nameof(HasMultipleItemSelections));
|
|
||||||
|
|
||||||
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
|
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
|
||||||
}
|
}
|
||||||
@@ -435,9 +436,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem)
|
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>
|
/// <summary>
|
||||||
@@ -447,9 +448,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ExecuteMailOperation(MailOperation mailOperation)
|
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)
|
private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable<MailItemViewModel> mailItems)
|
||||||
@@ -564,7 +565,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
var viewModels = PrepareMailViewModels(items);
|
var viewModels = PrepareMailViewModels(items);
|
||||||
|
|
||||||
await ExecuteUIThread(() => { MailCollection.AddEmails(viewModels); });
|
await MailCollection.AddRangeAsync(viewModels, false);
|
||||||
await ExecuteUIThread(() => { IsInitializingFolder = false; });
|
await ExecuteUIThread(() => { IsInitializingFolder = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,6 +586,14 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void RemoveFirst()
|
||||||
|
{
|
||||||
|
var fi = MailCollection.GetFirst();
|
||||||
|
|
||||||
|
Messenger.Send(new MailRemovedMessage(fi.MailCopy));
|
||||||
|
}
|
||||||
|
|
||||||
protected override async void OnMailAdded(MailCopy addedMail)
|
protected override async void OnMailAdded(MailCopy addedMail)
|
||||||
{
|
{
|
||||||
base.OnMailAdded(addedMail);
|
base.OnMailAdded(addedMail);
|
||||||
@@ -610,9 +619,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
await listManipulationSemepahore.WaitAsync();
|
await listManipulationSemepahore.WaitAsync();
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(async () =>
|
||||||
{
|
{
|
||||||
MailCollection.AddEmail(new MailItemViewModel(addedMail));
|
await MailCollection.AddAsync(addedMail);
|
||||||
NotifyItemFoundState();
|
NotifyItemFoundState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -629,8 +638,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(); });
|
||||||
}
|
}
|
||||||
@@ -656,7 +664,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction)
|
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.
|
// Automatically select the next item in the list if the setting is enabled.
|
||||||
MailItemViewModel nextItem = null;
|
MailItemViewModel nextItem = null;
|
||||||
@@ -672,7 +680,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
// Remove the deleted item from the list.
|
// Remove the deleted item from the list.
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
MailCollection.RemoveEmailByMailCopy(removedMail);
|
_ = MailCollection.RemoveAsync(removedMail);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextItem != null)
|
if (nextItem != null)
|
||||||
@@ -684,7 +692,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
await ExecuteUIThread(() =>
|
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.
|
// Otherwise the draft mail item will be duplicated on the next add execution.
|
||||||
await listManipulationSemepahore.WaitAsync();
|
await listManipulationSemepahore.WaitAsync();
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(async () =>
|
||||||
{
|
{
|
||||||
// Create the item. Draft folder navigation is already done at this point.
|
// 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.
|
// New draft is created by user. Select the item.
|
||||||
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
|
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
|
||||||
@@ -745,7 +753,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MailCollection.Clear();
|
_ = MailCollection.ClearAsync();
|
||||||
|
|
||||||
if (ActiveFolder == null)
|
if (ActiveFolder == null)
|
||||||
return;
|
return;
|
||||||
@@ -847,10 +855,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
var viewModels = PrepareMailViewModels(items);
|
var viewModels = PrepareMailViewModels(items);
|
||||||
|
|
||||||
|
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
MailCollection.AddEmails(viewModels);
|
|
||||||
|
|
||||||
if (isDoingSearch && !isDoingOnlineSearch)
|
if (isDoingSearch && !isDoingOnlineSearch)
|
||||||
{
|
{
|
||||||
IsOnlineSearchButtonVisible = true;
|
IsOnlineSearchButtonVisible = true;
|
||||||
@@ -1059,21 +1067,25 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
if (handlingFolder == null) return;
|
if (handlingFolder == null) return;
|
||||||
|
|
||||||
_ = ExecuteUIThread(() =>
|
_ = ExecuteUIThread(async () =>
|
||||||
{
|
{
|
||||||
MailCollection.Clear();
|
await MailCollection.ClearAsync();
|
||||||
|
|
||||||
_mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning);
|
_mailDialogService.InfoBarMessage(Translator.AccountCacheReset_Title, Translator.AccountCacheReset_Message, InfoBarMessageType.Warning);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnDispatcherAssigned()
|
||||||
|
{
|
||||||
|
base.OnDispatcherAssigned();
|
||||||
|
|
||||||
|
MailCollection.CoreDispatcher = Dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(ThumbnailAdded message)
|
public void Receive(ThumbnailAdded message)
|
||||||
{
|
{
|
||||||
Dispatcher.ExecuteOnUIThread(() =>
|
_ = MailCollection.UpdateThumbnailsForAddressAsync(message.Email);
|
||||||
{
|
|
||||||
MailCollection.UpdateThumbnailsForAddress(message.Email);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RegisterRecipients()
|
protected override void RegisterRecipients()
|
||||||
|
|||||||
@@ -19,4 +19,7 @@
|
|||||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Helpers\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -12,9 +12,11 @@
|
|||||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
<uwp:CoreGeneric />
|
<uwp:CoreGeneric />
|
||||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
|
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
|
||||||
|
<ResourceDictionary Source="Controls/ListView/WinoListViewStyles.xaml" />
|
||||||
<ResourceDictionary Source="Styles/ItemContainerStyles.xaml" />
|
<ResourceDictionary Source="Styles/ItemContainerStyles.xaml" />
|
||||||
<ResourceDictionary Source="Styles/ImagePreviewControl.xaml" />
|
<ResourceDictionary Source="Styles/ImagePreviewControl.xaml" />
|
||||||
<ResourceDictionary Source="Styles/WebViewEditorControl.xaml" />
|
<ResourceDictionary Source="Styles/WebViewEditorControl.xaml" />
|
||||||
|
<ResourceDictionary Source="Controls/ListView/WinoListViewStyles.xaml" />
|
||||||
|
|
||||||
<styles:WinoExpanderStyle />
|
<styles:WinoExpanderStyle />
|
||||||
<ResourceDictionary Source="/Wino.Core.WinUI/AppThemes/Default.xaml" />
|
<ResourceDictionary Source="/Wino.Core.WinUI/AppThemes/Default.xaml" />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@@ -6,6 +7,7 @@ using Wino.Mail.ViewModels.Data;
|
|||||||
|
|
||||||
namespace Wino.Mail.WinUI.Controls.Advanced;
|
namespace Wino.Mail.WinUI.Controls.Advanced;
|
||||||
|
|
||||||
|
[Obsolete("ItemsView sucks. Hard to deal with virtualization issues. Use ListView. This control is here to wise up anyone who tries to use it.")]
|
||||||
public partial class WinoItemsView : ItemsView
|
public partial class WinoItemsView : ItemsView
|
||||||
{
|
{
|
||||||
private const string PART_ScrollView = nameof(PART_ScrollView);
|
private const string PART_ScrollView = nameof(PART_ScrollView);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls.ListView;
|
||||||
|
|
||||||
|
public partial class WinoListView : Microsoft.UI.Xaml.Controls.ListView
|
||||||
|
{
|
||||||
|
public bool IsAllSelected => Items.Count == SelectedItems.Count;
|
||||||
|
|
||||||
|
protected override DependencyObject GetContainerForItemOverride() => new WinoListViewItem();
|
||||||
|
|
||||||
|
public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel)
|
||||||
|
{
|
||||||
|
WinoListViewItem? itemContainer = null;
|
||||||
|
|
||||||
|
foreach (var item in Items)
|
||||||
|
{
|
||||||
|
if (item is MailItemViewModel mailItem && mailItem.Id == mailItemViewModel.Id)
|
||||||
|
{
|
||||||
|
itemContainer = ContainerFromItem(mailItemViewModel) as WinoListViewItem;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailItemViewModel.MailCopy.UniqueId))
|
||||||
|
{
|
||||||
|
itemContainer = ContainerFromItem(threadMailItemViewModel) as WinoListViewItem;
|
||||||
|
|
||||||
|
// Try to get the inner WinoListView.
|
||||||
|
if (itemContainer != null)
|
||||||
|
{
|
||||||
|
itemContainer.IsExpanded = true;
|
||||||
|
|
||||||
|
var innerListViewControl = itemContainer.GetWinoListViewControl();
|
||||||
|
|
||||||
|
if (innerListViewControl != null)
|
||||||
|
{
|
||||||
|
itemContainer = innerListViewControl.ContainerFromItem(mailItemViewModel) as WinoListViewItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemContainer?.IsSelected = true;
|
||||||
|
|
||||||
|
return itemContainer != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
using Wino.Messaging.Client.Mails;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls.ListView;
|
||||||
|
|
||||||
|
public partial class WinoListViewItem : ListViewItem
|
||||||
|
{
|
||||||
|
public bool IsExpanded
|
||||||
|
{
|
||||||
|
get { return (bool)GetValue(IsExpandedProperty); }
|
||||||
|
set { SetValue(IsExpandedProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(WinoListViewItem), new PropertyMetadata(false, OnIsExpandedChanged));
|
||||||
|
|
||||||
|
public WinoListViewItem()
|
||||||
|
{
|
||||||
|
DefaultStyleKey = typeof(WinoListViewItem);
|
||||||
|
|
||||||
|
RegisterPropertyChangedCallback(IsSelectedProperty, OnIsSelectedChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnIsExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is WinoListViewItem item)
|
||||||
|
{
|
||||||
|
// Handle expansion state change if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnContentChanged(object oldContent, object newContent)
|
||||||
|
{
|
||||||
|
base.OnContentChanged(oldContent, newContent);
|
||||||
|
|
||||||
|
if (oldContent is IMailListItem oldMailItem)
|
||||||
|
{
|
||||||
|
UnregisterSelectionCallback(oldMailItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContent is IMailListItem newMailItem)
|
||||||
|
{
|
||||||
|
IsSelected = newMailItem.IsSelected;
|
||||||
|
RegisterSelectionCallback(newMailItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterSelectionCallback(IMailListItem mailItem)
|
||||||
|
{
|
||||||
|
mailItem.PropertyChanged -= MailPropChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterSelectionCallback(IMailListItem mailItem)
|
||||||
|
{
|
||||||
|
mailItem.PropertyChanged += MailPropChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From model
|
||||||
|
private void MailPropChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not IMailListItem mailItem) return;
|
||||||
|
|
||||||
|
if (e.PropertyName == nameof(IMailListItem.IsSelected)) ApplySelectionForContainer(mailItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From container.
|
||||||
|
private void OnIsSelectedChanged(DependencyObject sender, DependencyProperty dp)
|
||||||
|
{
|
||||||
|
if (Content is IMailListItem mailItem)
|
||||||
|
{
|
||||||
|
ApplySelectionForModel(mailItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySelectionForModel(IMailListItem mailItem)
|
||||||
|
{
|
||||||
|
if (mailItem.IsSelected != IsSelected)
|
||||||
|
{
|
||||||
|
mailItem.IsSelected = IsSelected;
|
||||||
|
WeakReferenceMessenger.Default.Send(new SelectedItemsChangedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySelectionForContainer(IMailListItem mailItem)
|
||||||
|
{
|
||||||
|
if (IsSelected != mailItem.IsSelected)
|
||||||
|
{
|
||||||
|
IsSelected = mailItem.IsSelected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WinoListView? GetWinoListViewControl()
|
||||||
|
{
|
||||||
|
var expander = GetTemplateChild("ExpanderPart") as Expander;
|
||||||
|
|
||||||
|
if (expander?.Content is ContentPresenter presenter) return VisualTreeHelper.GetChild(presenter, 0) as WinoListView;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Wino.Mail.WinUI.Controls.ListView">
|
||||||
|
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
|
|
||||||
|
<ResourceDictionary>
|
||||||
|
<!-- Thread Mail ListViewItem Style -->
|
||||||
|
<Style x:Key="DefaultThreadListViewItemStyle" TargetType="local:WinoListViewItem">
|
||||||
|
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||||
|
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||||
|
<Setter Property="Background" Value="{ThemeResource ListViewItemBackground}" />
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource ListViewItemForeground}" />
|
||||||
|
<Setter Property="TabNavigation" Value="Local" />
|
||||||
|
<Setter Property="IsHoldingEnabled" Value="True" />
|
||||||
|
<Setter Property="Padding" Value="16,0,12,0" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="MinWidth" Value="{ThemeResource ListViewItemMinWidth}" />
|
||||||
|
<Setter Property="MinHeight" Value="{ThemeResource ListViewItemMinHeight}" />
|
||||||
|
<Setter Property="AllowDrop" Value="False" />
|
||||||
|
<Setter Property="UseSystemFocusVisuals" Value="True" />
|
||||||
|
<Setter Property="FocusVisualMargin" Value="1" />
|
||||||
|
<Setter Property="FocusVisualPrimaryBrush" Value="{ThemeResource ListViewItemFocusVisualPrimaryBrush}" />
|
||||||
|
<Setter Property="FocusVisualPrimaryThickness" Value="2" />
|
||||||
|
<Setter Property="FocusVisualSecondaryBrush" Value="{ThemeResource ListViewItemFocusVisualSecondaryBrush}" />
|
||||||
|
<Setter Property="FocusVisualSecondaryThickness" Value="1" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="local:WinoListViewItem">
|
||||||
|
<Expander Header="Thread" IsExpanded="{TemplateBinding IsExpanded}">
|
||||||
|
<Expander.Content>
|
||||||
|
<!-- Expandable Content -->
|
||||||
|
<ContentPresenter
|
||||||
|
x:Name="ThreadContent"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
ContentTransitions="{TemplateBinding ContentTransitions}" />
|
||||||
|
</Expander.Content>
|
||||||
|
</Expander>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Default Single Mail List View Item Style. -->
|
||||||
|
<Style
|
||||||
|
x:Key="DefaultMailListViewItemStyle"
|
||||||
|
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||||
|
TargetType="local:WinoListViewItem" />
|
||||||
|
|
||||||
|
<local:WinoMailItemContainerStyleSelector
|
||||||
|
x:Name="WinoMailItemContainerStyleSelector"
|
||||||
|
MailItemStyle="{StaticResource DefaultMailListViewItemStyle}"
|
||||||
|
ThreadStyle="{StaticResource DefaultThreadListViewItemStyle}" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls.ListView;
|
||||||
|
|
||||||
|
public partial class WinoMailItemContainerStyleSelector : StyleSelector
|
||||||
|
{
|
||||||
|
public Style? ThreadStyle { get; set; }
|
||||||
|
public Style? MailItemStyle { get; set; }
|
||||||
|
protected override Style SelectStyleCore(object item, DependencyObject container)
|
||||||
|
{
|
||||||
|
if (item is MailItemViewModel) return MailItemStyle ?? throw new Exception($"Missing style for {nameof(MailItemViewModel)}");
|
||||||
|
if (item is ThreadMailItemViewModel) return ThreadStyle ?? throw new Exception($"Missing style for {nameof(ThreadMailItemViewModel)}");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls.ListView;
|
||||||
|
|
||||||
|
public partial class WinoMailItemTemplateSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public DataTemplate? SingleMailItemTemplate { get; set; }
|
||||||
|
public DataTemplate? ThreadMailItemTemplate { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||||
|
{
|
||||||
|
if (item is MailItemViewModel)
|
||||||
|
return SingleMailItemTemplate ?? throw new Exception($"Missing template for single mail items.");
|
||||||
|
else if (item is ThreadMailItemViewModel)
|
||||||
|
return ThreadMailItemTemplate ?? throw new Exception($"Missing template for thread mail items.");
|
||||||
|
|
||||||
|
return base.SelectTemplateCore(item, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Wino.Mail.ViewModels.Data;
|
|
||||||
|
|
||||||
namespace Wino.Selectors;
|
|
||||||
|
|
||||||
public partial class MailItemContainerStyleSelector : StyleSelector
|
|
||||||
{
|
|
||||||
public Style Thread { get; set; }
|
|
||||||
|
|
||||||
protected override Style SelectStyleCore(object item, DependencyObject container)
|
|
||||||
{
|
|
||||||
if (item is ThreadMailItemViewModel)
|
|
||||||
return Thread;
|
|
||||||
else
|
|
||||||
return base.SelectStyleCore(item, container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Wino.Mail.ViewModels.Data;
|
|
||||||
|
|
||||||
namespace Wino.Selectors;
|
|
||||||
|
|
||||||
public partial class MailItemDisplaySelector : DataTemplateSelector
|
|
||||||
{
|
|
||||||
public DataTemplate SingleMailItemTemplate { get; set; }
|
|
||||||
public DataTemplate ThreadMailItemTemplate { get; set; }
|
|
||||||
|
|
||||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
|
||||||
{
|
|
||||||
if (item is MailItemViewModel)
|
|
||||||
return SingleMailItemTemplate;
|
|
||||||
else if (item is ThreadMailItemViewModel)
|
|
||||||
return ThreadMailItemTemplate;
|
|
||||||
|
|
||||||
return base.SelectTemplateCore(item, container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -234,8 +234,14 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
|
|
||||||
FocusManager.GotFocus += GlobalFocusManagerGotFocus;
|
FocusManager.GotFocus += GlobalFocusManagerGotFocus;
|
||||||
|
|
||||||
var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation");
|
var webView = GetWebView();
|
||||||
anim?.TryStart(GetWebView());
|
|
||||||
|
if (webView != null)
|
||||||
|
{
|
||||||
|
var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation");
|
||||||
|
|
||||||
|
anim?.TryStart(webView);
|
||||||
|
}
|
||||||
|
|
||||||
_disposables.Add(GetSuggestionBoxDisposable(ToBox));
|
_disposables.Add(GetSuggestionBoxDisposable(ToBox));
|
||||||
_disposables.Add(GetSuggestionBoxDisposable(CCBox));
|
_disposables.Add(GetSuggestionBoxDisposable(CCBox));
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:i="using:Microsoft.Xaml.Interactivity"
|
xmlns:i="using:Microsoft.Xaml.Interactivity"
|
||||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||||
|
xmlns:listview="using:Wino.Mail.WinUI.Controls.ListView"
|
||||||
xmlns:local="using:Wino.Behaviors"
|
xmlns:local="using:Wino.Behaviors"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:menuflyouts="using:Wino.MenuFlyouts"
|
xmlns:menuflyouts="using:Wino.MenuFlyouts"
|
||||||
@@ -49,147 +50,55 @@
|
|||||||
|
|
||||||
<SolidColorBrush x:Key="ButtonBackgroundDisabled">Transparent</SolidColorBrush>
|
<SolidColorBrush x:Key="ButtonBackgroundDisabled">Transparent</SolidColorBrush>
|
||||||
|
|
||||||
<!--
|
<!-- Single Mail Item Template -->
|
||||||
Virtualization is disabled because it messes up the selected items.
|
<DataTemplate x:Key="SingleMailItemTemplate" x:DataType="viewModelData:MailItemViewModel">
|
||||||
When item is unrealized, it's selection state is lost.
|
<controls:MailItemDisplayInformationControl
|
||||||
I don't want to intercept ItemContainer for it.
|
x:DefaultBindMode="OneWay"
|
||||||
-->
|
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
|
||||||
|
ContextRequested="MailItemContextRequested"
|
||||||
<StackLayout
|
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
|
||||||
x:Key="DefaultItemsViewLayout"
|
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
|
||||||
IsVirtualizationEnabled="False"
|
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
|
||||||
Orientation="Vertical"
|
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
|
||||||
Spacing="6" />
|
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
|
||||||
|
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
|
||||||
<!-- Templates -->
|
MailItem="{x:Bind MailCopy, Mode=OneWay}"
|
||||||
<DataTemplate x:Key="EmailTemplate" x:DataType="viewModelData:MailItemViewModel">
|
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
|
||||||
<ItemContainer
|
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
|
||||||
HorizontalContentAlignment="Stretch"
|
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
|
||||||
VerticalContentAlignment="Stretch"
|
|
||||||
CanUserInvoke="UserCanInvoke"
|
|
||||||
IsSelected="{x:Bind IsSelected, Mode=TwoWay}">
|
|
||||||
|
|
||||||
<controls:MailItemDisplayInformationControl
|
|
||||||
Margin="{x:Bind helpers:XamlHelpers.GetMailItemControlMargin(IsDisplayedInThread), Mode=OneWay}"
|
|
||||||
x:DefaultBindMode="OneWay"
|
|
||||||
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
|
|
||||||
ContextRequested="MailItemContextRequested"
|
|
||||||
DataContext="{x:Bind}"
|
|
||||||
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
|
|
||||||
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
|
|
||||||
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
|
|
||||||
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
|
|
||||||
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
|
|
||||||
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
|
|
||||||
MailItem="{x:Bind MailCopy, Mode=OneWay}"
|
|
||||||
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
|
|
||||||
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
|
|
||||||
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
|
|
||||||
</ItemContainer>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="ThreadExpanderTemplate" x:DataType="viewModelData:ThreadMailItemViewModel">
|
<DataTemplate x:Key="ThreadMailItemTemplate" x:DataType="viewModelData:ThreadMailItemViewModel">
|
||||||
<ItemContainer
|
<TextBlock Text="thread :)" />
|
||||||
CanUserSelect="UserCannotSelect"
|
|
||||||
RightTapped="ThreadContainerRightTapped"
|
|
||||||
Tag="{x:Bind}"
|
|
||||||
Tapped="ThreadContainerTapped">
|
|
||||||
<Grid Padding="4,0,0,0" ColumnSpacing="8">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Expansion indicator -->
|
|
||||||
<Viewbox Width="12" Height="12">
|
|
||||||
<FontIcon
|
|
||||||
x:Name="ExpanderIcon"
|
|
||||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
|
||||||
FontSize="12"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Glyph="" />
|
|
||||||
</Viewbox>
|
|
||||||
|
|
||||||
<controls:MailItemDisplayInformationControl
|
|
||||||
Grid.Column="1"
|
|
||||||
x:DefaultBindMode="OneWay"
|
|
||||||
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
|
|
||||||
ContextRequested="MailItemContextRequested"
|
|
||||||
DataContext="{x:Bind}"
|
|
||||||
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
|
|
||||||
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
|
|
||||||
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
|
|
||||||
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
|
|
||||||
IsThumbnailUpdated="{x:Bind LatestMailViewModel.ThumbnailUpdatedEvent, Mode=OneWay}"
|
|
||||||
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
|
|
||||||
MailItem="{x:Bind LatestMailViewModel.MailCopy, Mode=OneWay}"
|
|
||||||
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
|
|
||||||
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
|
|
||||||
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
|
|
||||||
</Grid>
|
|
||||||
</ItemContainer>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="DateGroupHeaderTemplate" x:DataType="data:DateGroupHeader">
|
<listview:WinoMailItemTemplateSelector
|
||||||
<ItemContainer
|
x:Key="MailItemTemplateSelector"
|
||||||
CanUserSelect="UserCannotSelect"
|
SingleMailItemTemplate="{StaticResource SingleMailItemTemplate}"
|
||||||
IsHitTestVisible="False"
|
ThreadMailItemTemplate="{StaticResource ThreadMailItemTemplate}" />
|
||||||
IsSelected="False">
|
|
||||||
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
|
|
||||||
<TextBlock
|
|
||||||
FontSize="14"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
|
||||||
Opacity="1"
|
|
||||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
|
||||||
Text="{x:Bind DisplayName, Mode=OneWay}" />
|
|
||||||
</Grid>
|
|
||||||
</ItemContainer>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<DataTemplate x:Key="SenderGroupHeaderTemplate" x:DataType="data:SenderGroupHeader">
|
<CollectionViewSource
|
||||||
<ItemContainer
|
x:Name="MailCollectionViewSource"
|
||||||
CanUserSelect="UserCannotSelect"
|
IsSourceGrouped="True"
|
||||||
IsEnabled="False"
|
Source="{x:Bind ViewModel.MailCollection.MailItems, Mode=OneWay}" />
|
||||||
IsHitTestVisible="False">
|
|
||||||
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock
|
|
||||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
|
||||||
Style="{ThemeResource SubtitleTextBlockStyle}"
|
|
||||||
Text="{x:Bind DisplayName, Mode=OneWay}" />
|
|
||||||
<StackPanel
|
|
||||||
Grid.Column="1"
|
|
||||||
Orientation="Horizontal"
|
|
||||||
Spacing="8">
|
|
||||||
<TextBlock
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
|
||||||
Text="{x:Bind ItemCount, Mode=OneWay}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</ItemContainer>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<selectors1:MailItemContainerSelector
|
<DataTemplate x:Key="MailGroupHeaderDefaultTemplate" x:DataType="collections:IReadOnlyObservableGroup">
|
||||||
x:Name="MailItemContainerSelector"
|
<Grid
|
||||||
DateGroupHeaderTemplate="{StaticResource DateGroupHeaderTemplate}"
|
Margin="4,2"
|
||||||
EmailTemplate="{StaticResource EmailTemplate}"
|
AllowFocusOnInteraction="False"
|
||||||
SenderGroupHeaderTemplate="{StaticResource SenderGroupHeaderTemplate}"
|
Background="{ThemeResource MailListHeaderBackgroundColor}"
|
||||||
ThreadExpanderTemplate="{StaticResource ThreadExpanderTemplate}" />
|
CornerRadius="6">
|
||||||
|
<TextBlock
|
||||||
|
Padding="12"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AllowFocusOnInteraction="False"
|
||||||
|
FontSize="13"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{x:Bind helpers:XamlHelpers.GetMailGroupDateString(Key)}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
</Page.Resources>
|
</Page.Resources>
|
||||||
|
|
||||||
<Page.KeyboardAccelerators>
|
|
||||||
<KeyboardAccelerator
|
|
||||||
Key="A"
|
|
||||||
Invoked="SelectAllInvoked"
|
|
||||||
Modifiers="Control" />
|
|
||||||
|
|
||||||
<KeyboardAccelerator Key="Delete" Invoked="DeleteAllInvoked" />
|
|
||||||
</Page.KeyboardAccelerators>
|
|
||||||
<wino:BasePage.ShellContent>
|
<wino:BasePage.ShellContent>
|
||||||
<Grid HorizontalAlignment="Stretch">
|
<Grid HorizontalAlignment="Stretch">
|
||||||
<!-- Hidden focus receiver... -->
|
<!-- Hidden focus receiver... -->
|
||||||
@@ -257,7 +166,7 @@
|
|||||||
<CommandBar
|
<CommandBar
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
DefaultLabelPosition="Collapsed"
|
DefaultLabelPosition="Collapsed"
|
||||||
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedVisibleItems.Count), Mode=OneWay}"
|
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedItemsCount), 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}" />
|
||||||
@@ -286,7 +195,6 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Canvas.ZIndex="100"
|
Canvas.ZIndex="100"
|
||||||
Checked="SelectAllCheckboxChecked"
|
Checked="SelectAllCheckboxChecked"
|
||||||
IsChecked="{x:Bind ViewModel.MailCollection.IsAllItemsSelected, Mode=OneWay}"
|
|
||||||
Unchecked="SelectAllCheckboxUnchecked" />
|
Unchecked="SelectAllCheckboxUnchecked" />
|
||||||
|
|
||||||
|
|
||||||
@@ -329,6 +237,7 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
|
<Button Command="{x:Bind ViewModel.RemoveFirstCommand}" Content="T" />
|
||||||
<Button
|
<Button
|
||||||
Width="36"
|
Width="36"
|
||||||
Height="36"
|
Height="36"
|
||||||
@@ -436,7 +345,37 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<advanced:WinoItemsView
|
<listview:WinoListView
|
||||||
|
x:Name="MailListView"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
|
||||||
|
toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0"
|
||||||
|
ItemContainerStyleSelector="{StaticResource WinoMailItemContainerStyleSelector}"
|
||||||
|
ItemTemplateSelector="{StaticResource MailItemTemplateSelector}"
|
||||||
|
ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}"
|
||||||
|
ProcessKeyboardAccelerators="WinoListViewProcessKeyboardAccelerators"
|
||||||
|
SelectionMode="Extended">
|
||||||
|
<listview:WinoListView.ItemContainerTransitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<AddDeleteThemeTransition />
|
||||||
|
</TransitionCollection>
|
||||||
|
</listview:WinoListView.ItemContainerTransitions>
|
||||||
|
<listview:WinoListView.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<ItemsStackPanel Margin="8,0,12,0" AreStickyGroupHeadersEnabled="True" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</listview:WinoListView.ItemsPanel>
|
||||||
|
<listview:WinoListView.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<Style BasedOn="{StaticResource MailListHeaderStyle}" TargetType="ListViewHeaderItem" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</listview:WinoListView.Resources>
|
||||||
|
<listview:WinoListView.GroupStyle>
|
||||||
|
<GroupStyle HeaderTemplate="{StaticResource MailGroupHeaderDefaultTemplate}" HidesIfEmpty="True" />
|
||||||
|
</listview:WinoListView.GroupStyle>
|
||||||
|
</listview:WinoListView>
|
||||||
|
<!--<advanced:WinoItemsView
|
||||||
x:Name="MailListView"
|
x:Name="MailListView"
|
||||||
Margin="4"
|
Margin="4"
|
||||||
ItemTemplate="{StaticResource MailItemContainerSelector}"
|
ItemTemplate="{StaticResource MailItemContainerSelector}"
|
||||||
@@ -444,7 +383,7 @@
|
|||||||
Layout="{StaticResource DefaultItemsViewLayout}"
|
Layout="{StaticResource DefaultItemsViewLayout}"
|
||||||
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
|
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
|
||||||
ProcessKeyboardAccelerators="MailListView_ProcessKeyboardAccelerators"
|
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}">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@@ -14,7 +15,6 @@ using Microsoft.UI.Xaml.Navigation;
|
|||||||
using MoreLinq;
|
using MoreLinq;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
using Wino.Controls;
|
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
@@ -24,7 +24,6 @@ using Wino.Core.Domain.Models.Navigation;
|
|||||||
using Wino.Helpers;
|
using Wino.Helpers;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Mail.ViewModels.Messages;
|
using Wino.Mail.ViewModels.Messages;
|
||||||
using Wino.Mail.WinUI;
|
|
||||||
using Wino.MenuFlyouts.Context;
|
using Wino.MenuFlyouts.Context;
|
||||||
using Wino.Messaging.Client.Mails;
|
using Wino.Messaging.Client.Mails;
|
||||||
using Wino.Views.Abstract;
|
using Wino.Views.Abstract;
|
||||||
@@ -39,8 +38,8 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
{
|
{
|
||||||
private const double RENDERING_COLUMN_MIN_WIDTH = 375;
|
private const double RENDERING_COLUMN_MIN_WIDTH = 375;
|
||||||
|
|
||||||
private IStatePersistanceService StatePersistenceService { get; } = App.Current.Services.GetService<IStatePersistanceService>();
|
private IStatePersistanceService StatePersistenceService { get; } = Core.WinUI.WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
|
||||||
private IKeyPressService KeyPressService { get; } = App.Current.Services.GetService<IKeyPressService>();
|
private IKeyPressService KeyPressService { get; } = Core.WinUI.WinoApplication.Current.Services.GetService<IKeyPressService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
|
||||||
|
|
||||||
public MailListPage()
|
public MailListPage()
|
||||||
{
|
{
|
||||||
@@ -53,27 +52,26 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
|
|
||||||
Bindings.Update();
|
Bindings.Update();
|
||||||
|
|
||||||
|
ViewModel.MailCollection.ItemSelectionChanged += WinoMailCollectionSelectionChanged;
|
||||||
|
|
||||||
|
UpdateSelectAllButtonStatus();
|
||||||
|
UpdateAdaptiveness();
|
||||||
|
|
||||||
// Delegate to ViewModel.
|
// Delegate to ViewModel.
|
||||||
if (e.Parameter is NavigateMailFolderEventArgs folderNavigationArgs)
|
if (e.Parameter is NavigateMailFolderEventArgs folderNavigationArgs)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
|
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewModel.ThrottledSelectionChanged += ListSelectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ListSelectionChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
UpdateSelectAllButtonStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnNavigatedFrom(e);
|
base.OnNavigatedFrom(e);
|
||||||
|
|
||||||
ViewModel.ThrottledSelectionChanged -= ListSelectionChanged;
|
|
||||||
this.Bindings.StopTracking();
|
this.Bindings.StopTracking();
|
||||||
|
|
||||||
|
ViewModel.MailCollection.ItemSelectionChanged -= WinoMailCollectionSelectionChanged;
|
||||||
|
|
||||||
RenderingFrame.Navigate(typeof(IdlePage));
|
RenderingFrame.Navigate(typeof(IdlePage));
|
||||||
|
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
@@ -84,20 +82,19 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
// Check all checkbox if all is selected.
|
// Check all checkbox if all is selected.
|
||||||
// Unhook events to prevent selection overriding.
|
// Unhook events to prevent selection overriding.
|
||||||
|
|
||||||
//DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||||
//{
|
{
|
||||||
// SelectAllCheckbox.Checked -= SelectAllCheckboxChecked;
|
SelectAllCheckbox.Checked -= SelectAllCheckboxChecked;
|
||||||
// SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked;
|
SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked;
|
||||||
|
|
||||||
// bool isAllSelected = ViewModel.MailCollection.AllItems.All(a => a.IsSelected);
|
SelectAllCheckbox.IsChecked = ViewModel.MailCollection.IsAllItemsSelected;
|
||||||
// SelectAllCheckbox.IsChecked = ViewModel.MailCollection.AllItems.Count() > 0 && isAllSelected;
|
|
||||||
|
|
||||||
// SelectAllCheckbox.Checked += SelectAllCheckboxChecked;
|
SelectAllCheckbox.Checked += SelectAllCheckboxChecked;
|
||||||
// SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
|
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
|
||||||
//});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectionModeToggleChecked(object sender, RoutedEventArgs e) => ChangeSelectionMode(ItemsViewSelectionMode.Multiple);
|
private void SelectionModeToggleChecked(object sender, RoutedEventArgs e) => ChangeSelectionMode(ListViewSelectionMode.Multiple);
|
||||||
|
|
||||||
private void FolderPivotChanged(object sender, SelectionChangedEventArgs e)
|
private void FolderPivotChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -124,29 +121,29 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
ViewModel.SelectedPivotChangedCommand.Execute(null);
|
ViewModel.SelectedPivotChangedCommand.Execute(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ChangeSelectionMode(ItemsViewSelectionMode mode)
|
private void ChangeSelectionMode(ListViewSelectionMode mode)
|
||||||
{
|
{
|
||||||
MailListView.SelectionMode = mode;
|
MailListView.SelectionMode = mode;
|
||||||
|
|
||||||
if (ViewModel?.PivotFolders != null)
|
if (ViewModel?.PivotFolders != null)
|
||||||
{
|
{
|
||||||
ViewModel.PivotFolders.ForEach(a => a.IsExtendedMode = mode == ItemsViewSelectionMode.Extended);
|
ViewModel.PivotFolders.ForEach(a => a.IsExtendedMode = mode == ListViewSelectionMode.Extended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectionModeToggleUnchecked(object sender, RoutedEventArgs e)
|
private void SelectionModeToggleUnchecked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ChangeSelectionMode(ItemsViewSelectionMode.Extended);
|
ChangeSelectionMode(ListViewSelectionMode.Extended);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
|
private async void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.MailCollection.SelectAll();
|
await ViewModel.MailCollection.SelectAllAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
|
private async void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.MailCollection.ClearSelections();
|
await ViewModel.MailCollection.UnselectAllAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
||||||
@@ -156,24 +153,24 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
// Context is requested from a single mail point, but we might have multiple selected items.
|
// Context is requested from a single mail point, but we might have multiple selected items.
|
||||||
// This menu should be calculated based on all selected items by providers.
|
// This menu should be calculated based on all selected items by providers.
|
||||||
|
|
||||||
if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
|
//if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
|
||||||
{
|
//{
|
||||||
if (control.DataContext is MailItemViewModel clickedMailItemContext)
|
// if (control.DataContext is MailItemViewModel clickedMailItemContext)
|
||||||
{
|
// {
|
||||||
var targetItems = ViewModel.MailCollection.AllItems.Where(a => a.IsSelected);
|
// var targetItems = ViewModel.MailCollection.AllItems.Where(a => a.IsSelected);
|
||||||
var availableActions = ViewModel.GetAvailableMailActions(targetItems);
|
// var availableActions = ViewModel.GetAvailableMailActions(targetItems);
|
||||||
|
|
||||||
if (!availableActions?.Any() ?? false) return;
|
// if (!availableActions?.Any() ?? false) return;
|
||||||
|
|
||||||
var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
|
// var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
|
||||||
|
|
||||||
if (clickedOperation == null) return;
|
// if (clickedOperation == null) return;
|
||||||
|
|
||||||
var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
|
// var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
|
||||||
|
|
||||||
await ViewModel.ExecuteMailOperationAsync(prepRequest);
|
// await ViewModel.ExecuteMailOperationAsync(prepRequest);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<MailOperationMenuItem> GetMailOperationFromFlyoutAsync(IEnumerable<MailOperationMenuItem> availableActions,
|
private async Task<MailOperationMenuItem> GetMailOperationFromFlyoutAsync(IEnumerable<MailOperationMenuItem> availableActions,
|
||||||
@@ -277,7 +274,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
|
|
||||||
#region Connected Animation Helpers
|
#region Connected Animation Helpers
|
||||||
|
|
||||||
private WebView2 GetRenderingPageWebView()
|
private WebView2? GetRenderingPageWebView()
|
||||||
{
|
{
|
||||||
if (RenderingFrame.Content is MailRenderingPage renderingPage)
|
if (RenderingFrame.Content is MailRenderingPage renderingPage)
|
||||||
return renderingPage.GetWebView();
|
return renderingPage.GetWebView();
|
||||||
@@ -285,7 +282,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private WebView2 GetComposerPageWebView()
|
private WebView2? GetComposerPageWebView()
|
||||||
{
|
{
|
||||||
if (RenderingFrame.Content is ComposePage composePage)
|
if (RenderingFrame.Content is ComposePage composePage)
|
||||||
return composePage.GetWebView();
|
return composePage.GetWebView();
|
||||||
@@ -301,7 +298,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
|
|
||||||
await ViewModel.ExecuteUIThread(async () =>
|
await ViewModel.ExecuteUIThread(async () =>
|
||||||
{
|
{
|
||||||
MailListView.ClearSelections(message.SelectedMailViewModel, true);
|
// MailListView.ClearSelections(message.SelectedMailViewModel, true);
|
||||||
|
|
||||||
int retriedSelectionCount = 0;
|
int retriedSelectionCount = 0;
|
||||||
trySelection:
|
trySelection:
|
||||||
@@ -343,7 +340,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
|
|
||||||
if (scrollIndex >= 0)
|
if (scrollIndex >= 0)
|
||||||
{
|
{
|
||||||
MailListView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() { AnimationDesired = true });
|
await MailListView.SmoothScrollIntoViewWithIndexAsync(scrollIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -436,7 +433,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async void SearchBar_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
private async void SearchBar_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||||
{
|
{
|
||||||
// User clicked 'x' button to clearout the search text.
|
// User clicked 'x' button to clearout the search text.
|
||||||
@@ -510,13 +506,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
// MailListView.SelectAllWino();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||||
=> ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete);
|
=> ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete);
|
||||||
|
|
||||||
@@ -552,7 +541,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
visual.StartAnimation("RotationAngleInDegrees", rotationAnimation);
|
visual.StartAnimation("RotationAngleInDegrees", rotationAnimation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ListSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
|
private void WinoMailCollectionSelectionChanged(object sender, EventArgs args)
|
||||||
{
|
{
|
||||||
UpdateSelectAllButtonStatus();
|
UpdateSelectAllButtonStatus();
|
||||||
UpdateAdaptiveness();
|
UpdateAdaptiveness();
|
||||||
@@ -565,7 +554,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
expander.IsThreadExpanded = !expander.IsThreadExpanded;
|
expander.IsThreadExpanded = !expander.IsThreadExpanded;
|
||||||
|
|
||||||
// Select all.
|
// Select all.
|
||||||
ViewModel.MailCollection.AllItems.Where(a => expander.ThreadEmails.Contains(a)).ForEach(a => a.IsSelected = true);
|
// ViewModel.MailCollection.AllItems.Where(a => expander.ThreadEmails.Contains(a)).ForEach(a => a.IsSelected = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +575,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MailListView_ProcessKeyboardAccelerators(UIElement sender, ProcessKeyboardAcceleratorEventArgs args)
|
private async void WinoListViewProcessKeyboardAccelerators(UIElement sender, ProcessKeyboardAcceleratorEventArgs args)
|
||||||
{
|
{
|
||||||
if (args.Key == VirtualKey.Delete)
|
if (args.Key == VirtualKey.Delete)
|
||||||
{
|
{
|
||||||
@@ -594,22 +583,9 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// ItemsView have weird logic for selection inversion. We need to handle it manually.
|
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
|
|
||||||
// TODO: If not all items are selected, select all. Otherwise clear selection.
|
await ViewModel.MailCollection.ToggleSelectAllAsync();
|
||||||
// Handle selections in the GroupedEmailCollection.
|
|
||||||
|
|
||||||
ViewModel.MailCollection.SelectAll();
|
|
||||||
|
|
||||||
//if (!ViewModel.MailCollection.IsAllItemsSelected)
|
|
||||||
//{
|
|
||||||
|
|
||||||
//}
|
|
||||||
//else
|
|
||||||
//{
|
|
||||||
// ViewModel.MailCollection.ClearSelections();
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
<Content Remove="Assets\Wide310x150Logo.scale-400.png" />
|
<Content Remove="Assets\Wide310x150Logo.scale-400.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="Controls\ListView\WinoListViewStyles.xaml" />
|
||||||
<None Remove="ShellWindow.xaml" />
|
<None Remove="ShellWindow.xaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -123,6 +124,11 @@
|
|||||||
<ProjectReference Include="..\Wino.Mail.ViewModels\Wino.Mail.ViewModels.csproj" />
|
<ProjectReference Include="..\Wino.Mail.ViewModels\Wino.Mail.ViewModels.csproj" />
|
||||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Controls\ListView\WinoListViewStyles.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Update="ShellWindow.xaml">
|
<Page Update="ShellWindow.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Wino.Messaging.Client.Mails;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selection of items are handled in the view model collection.
|
||||||
|
/// This message notifies interested parties that the selection has changed.
|
||||||
|
/// </summary>
|
||||||
|
public record SelectedItemsChangedMessage;
|
||||||
Reference in New Issue
Block a user