From 449c1d3f4da7e6dfee16fa9160b4587777a13376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Tue, 21 Oct 2025 22:08:56 +0200 Subject: [PATCH] Fixing some issues with ItemsView and selections. --- Wino.Core/Services/WinoRequestProcessor.cs | 2 +- .../Collections/GroupedEmailCollection.cs | 439 +++++++++++++++++- .../Helpers/ThrottledEventHandler.cs | 93 ++++ Wino.Mail.ViewModels/MailListPageViewModel.cs | 181 ++++---- .../Messages/MailItemSelectedEvent.cs | 18 - .../Messages/MailItemSelectionRemovedEvent.cs | 16 - Wino.Mail.WinUI/App.xaml.cs | 3 +- .../MailAuthenticatorConfiguration.cs | 12 +- Wino.Mail.WinUI/Views/ComposePage.xaml.cs | 9 +- Wino.Mail.WinUI/Views/MailListPage.xaml | 13 +- Wino.Mail.WinUI/Views/MailListPage.xaml.cs | 58 ++- 11 files changed, 657 insertions(+), 187 deletions(-) create mode 100644 Wino.Mail.ViewModels/Helpers/ThrottledEventHandler.cs delete mode 100644 Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs delete mode 100644 Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs diff --git a/Wino.Core/Services/WinoRequestProcessor.cs b/Wino.Core/Services/WinoRequestProcessor.cs index 0055d2d8..04f1fc17 100644 --- a/Wino.Core/Services/WinoRequestProcessor.cs +++ b/Wino.Core/Services/WinoRequestProcessor.cs @@ -94,7 +94,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor var requests = new List(); // TODO: Fix: Collection was modified; enumeration operation may not execute - foreach (var item in preperationRequest.MailItems) + foreach (var item in preperationRequest.MailItems.ToList()) { var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution); diff --git a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs index 2c6f5a40..e8bf1a6c 100644 --- a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs @@ -6,6 +6,7 @@ using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging.Messages; +using Wino.Core.Domain.Entities.Mail; using Wino.Mail.ViewModels.Data; namespace Wino.Mail.ViewModels.Collections; @@ -34,12 +35,15 @@ public enum EmailSortDirection /// public partial class GroupedEmailCollection : ObservableObject, IRecipient>, IDisposable { + public event EventHandler SelectionChanged; + private readonly ObservableCollection _sourceItems = []; private readonly Dictionary _groupHeaders = []; private readonly Dictionary _groupHeaderIndexCache = []; private readonly Dictionary> _groupItems = []; private readonly Dictionary _threadExpanders = []; private readonly HashSet _mailCopyIdHashSet = []; + private readonly HashSet _selectedVisibleItems = []; private bool _disposed; private bool _isUpdating; @@ -49,6 +53,28 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient + /// Indicates whether there are any selected visible items. + /// + public bool HasSelectedItems => SelectedVisibleCount > 0; + + /// + /// Indicates whether there is exactly one selected visible item. + /// + public bool HasSingleItemSelected => SelectedVisibleCount == 1; + + /// + /// Indicates whether there are multiple selected visible items. + /// + public bool HasMultipleItemsSelected => SelectedVisibleCount > 1; + public GroupedEmailCollection() { // Create a flat collection for ItemsView with headers, expanders and emails mixed @@ -89,6 +115,12 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient public IEnumerable AllItems => _sourceItems; + /// + /// Gets all currently selected visible email items in the UI. + /// This collection is automatically maintained by tracking PropertyChanged events. + /// + public IReadOnlyCollection SelectedVisibleItems => _selectedVisibleItems; + /// /// Gets all currently selected email items. /// Includes: @@ -163,7 +195,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient - /// Handles PropertyChanged messages for thread expansion + /// Handles PropertyChanged messages for thread expansion and mail item selection /// public void Receive(PropertyChangedMessage message) { @@ -175,6 +207,94 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient + /// Registers a mail item to track its selection state when added to the visible UI + /// + private void RegisterMailItemForSelectionTracking(MailItemViewModel mailItem) + { + if (mailItem == null) + return; + + // Subscribe to property changed to track IsSelected changes + mailItem.PropertyChanged += MailItem_PropertyChanged; + + // If the item is already selected, add it to the tracking set + if (mailItem.IsSelected && Items.Contains(mailItem)) + { + if (_selectedVisibleItems.Add(mailItem)) + { + SelectedVisibleCount = _selectedVisibleItems.Count; + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + /// + /// Unregisters a mail item from selection tracking when removed from the visible UI + /// + private void UnregisterMailItemFromSelectionTracking(MailItemViewModel mailItem) + { + if (mailItem == null) + return; + + // Unsubscribe from property changed + mailItem.PropertyChanged -= MailItem_PropertyChanged; + + // Remove from selected items tracking + if (_selectedVisibleItems.Remove(mailItem)) + { + SelectedVisibleCount = _selectedVisibleItems.Count; + OnPropertyChanged(nameof(SelectedVisibleItems)); + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void MailItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (sender is MailItemViewModel mailItem && e.PropertyName == nameof(MailItemViewModel.IsSelected)) + { + HandleMailItemSelectionChanged(mailItem, mailItem.IsSelected); + } } private void HandleThreadExpansion(ThreadMailItemViewModel expander) @@ -197,6 +317,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient= 0) { - var sourceItem = _sourceItems.FirstOrDefault(a => a == email); - + UnregisterMailItemFromSelectionTracking(email); Items.RemoveAt(emailIndex); UpdateHeaderIndicesAfterRemoval(emailIndex); } @@ -315,17 +435,35 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient + /// Removes an email from the collection based on MailCopy. + /// The mail copy might be in a thread where it's not visible in the UI items. + /// In that case, it will be removed from the all items source and the thread. + /// + public void RemoveEmailByMailCopy(MailCopy mailCopy) + { + if (mailCopy == null) + return; + // Remove from unique ID tracking - _mailCopyIdHashSet.Remove(email.MailCopy.UniqueId); + _mailCopyIdHashSet.Remove(mailCopy.UniqueId); _isUpdating = true; try { - var threadId = email.MailCopy.ThreadId; + // Find the email in the source collection + var email = _sourceItems.FirstOrDefault(e => e.MailCopy.UniqueId == mailCopy.UniqueId); + + if (email == null) + return; // Email not found + + var threadId = mailCopy.ThreadId; // Remove from source collection - if (!_sourceItems.Remove(email)) - return; // Email not found + _sourceItems.Remove(email); if (!string.IsNullOrEmpty(threadId) && _threadExpanders.TryGetValue(threadId, out var expander)) { @@ -333,7 +471,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient 0; + _selectedVisibleItems.Clear(); + SelectedVisibleCount = 0; + // Reset IsDisplayedInThread for all emails before clearing foreach (var email in _sourceItems) { @@ -453,6 +605,12 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient 0; + _selectedVisibleItems.Clear(); + SelectedVisibleCount = 0; if (!_sourceItems.Any()) + { + if (hadSelectedItems) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } return; + } // Rebuild thread expanders based on current emails RebuildThreadExpanders(); @@ -550,6 +726,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient(); } - + // Insert in the group items list maintaining sort order var groupItems = _groupItems[groupKey]; var groupInsertIndex = FindGroupInsertionIndex(email, groupItems); groupItems.Insert(groupInsertIndex, email); - + // If this is the first item in a new group, we need to add the header if (groupItems.Count == 1) { @@ -623,7 +807,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient + /// Gets the next item in the UI list based on the given MailCopy. + /// If the next item is a thread, the thread will be expanded and the first item in the thread returned. + /// + /// The mail copy to find the next item for. + /// The next mail item in the UI list, or null if no next item exists. + public MailItemViewModel GetNextItem(MailCopy mailCopy) + { + if (mailCopy == null) + return null; + + _isUpdating = true; + try + { + // Find the current item in the UI Items collection + var currentItemIndex = -1; + MailItemViewModel currentMailItem = null; + ThreadMailItemViewModel currentThread = null; + + // First, try to find the item as a standalone email in the Items collection + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + + if (item is MailItemViewModel mailItem && mailItem.MailCopy.UniqueId == mailCopy.UniqueId) + { + currentItemIndex = i; + currentMailItem = mailItem; + break; + } + } + + // If not found as standalone, check if it's in a thread + if (currentItemIndex == -1) + { + // Find the thread that contains this mail + foreach (var expander in _threadExpanders.Values) + { + if (expander.HasUniqueId(mailCopy.UniqueId)) + { + // Find the thread expander in the Items collection + currentItemIndex = Items.IndexOf(expander); + currentThread = expander; + + // If thread is expanded, find the specific mail item within the thread + if (expander.IsThreadExpanded) + { + var threadMailItem = expander.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == mailCopy.UniqueId); + if (threadMailItem != null) + { + currentItemIndex = Items.IndexOf(threadMailItem); + currentMailItem = threadMailItem; + } + } + + break; + } + } + } + + // If we still haven't found the item, it's not in the UI + if (currentItemIndex == -1) + return null; + + // Look for the next item in the Items collection + for (int i = currentItemIndex + 1; i < Items.Count; i++) + { + var nextItem = Items[i]; + + // Skip group headers + if (nextItem is GroupHeaderBase) + continue; + + // If next item is a mail item, return it + if (nextItem is MailItemViewModel nextMailItem) + { + return nextMailItem; + } + + // If next item is a thread expander, expand it and return the first item + if (nextItem is ThreadMailItemViewModel threadExpander) + { + // Expand the thread if not already expanded + if (!threadExpander.IsThreadExpanded) + { + threadExpander.IsThreadExpanded = true; + } + + // Return the first item in the thread (latest email) + var sortedThreadEmails = SortDirection == EmailSortDirection.Descending + ? threadExpander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() + : threadExpander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); + + return sortedThreadEmails.FirstOrDefault(); + } + } + + // No next item found + return null; + } + finally + { + _isUpdating = false; + } + } + + /// + /// Searches for all mail items with a specific FromAddress and toggles their ThumbnailUpdatedEvent property. + /// This will notify the UI to update thumbnails for all matching items. + /// + /// The email address to search for in FromAddress property. + public void UpdateThumbnailsForAddress(string fromAddress) + { + if (string.IsNullOrEmpty(fromAddress)) return; + + // Search through all source items (includes both standalone and threaded emails) + foreach (var mailItem in _sourceItems) + { + if (string.Equals(mailItem.MailCopy.FromAddress, fromAddress, StringComparison.OrdinalIgnoreCase)) + { + // Toggle the ThumbnailUpdatedEvent to notify the UI + mailItem.ThumbnailUpdatedEvent = !mailItem.ThumbnailUpdatedEvent; + } + } + } + + /// + /// Selects all visible mail items in the collection. + /// Only operates on MailItemViewModel instances, skipping group headers and thread expanders. + /// + /// The number of items that were selected. + public int SelectAll() + { + var selectedCount = 0; + + foreach (var item in Items) + { + // Only select MailItemViewModel instances (skip GroupHeaderBase and ThreadMailItemViewModel) + if (item is MailItemViewModel mailItem) + { + if (!mailItem.IsSelected) + { + mailItem.IsSelected = true; + selectedCount++; + } + } + } + + SelectedVisibleCount = _selectedVisibleItems.Count; + + if (selectedCount > 0) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + + return selectedCount; + } + + /// + /// Clears the selection of all visible mail items in the collection. + /// Only operates on MailItemViewModel instances, skipping group headers and thread expanders. + /// + /// The number of items that were deselected. + public int ClearSelections() + { + var deselectedCount = 0; + + foreach (var item in Items) + { + // Only deselect MailItemViewModel instances (skip GroupHeaderBase and ThreadMailItemViewModel) + if (item is MailItemViewModel mailItem) + { + if (mailItem.IsSelected) + { + mailItem.IsSelected = false; + deselectedCount++; + } + } + } + + SelectedVisibleCount = _selectedVisibleItems.Count; + + if (deselectedCount > 0) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + + return deselectedCount; + } + private void AddThreadToUI(ThreadMailItemViewModel expander, string groupKey) { var groupHeader = GetOrCreateGroupHeader(groupKey); @@ -789,6 +1163,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient= 0) { + UnregisterMailItemFromSelectionTracking(email); Items.RemoveAt(emailIndex); UpdateHeaderIndicesAfterRemoval(emailIndex); } @@ -857,6 +1234,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient= 0) { + UnregisterMailItemFromSelectionTracking(email); Items.RemoveAt(itemIndex); UpdateHeaderIndicesAfterRemoval(itemIndex); } @@ -874,6 +1252,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient= 2) { // Create new thread expander var expander = new ThreadMailItemViewModel(threadId); _threadExpanders[threadId] = expander; - + // Add all emails to the thread foreach (var email in allThreadEmails) { @@ -1206,7 +1586,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient groupItems) { var emailDate = email.MailCopy?.CreationDate ?? DateTime.MinValue; - + for (int i = 0; i < groupItems.Count; i++) { var existingDate = GetEffectiveDate(groupItems[i]); var comparison = emailDate.CompareTo(existingDate); - + if (SortDirection == EmailSortDirection.Descending) comparison = -comparison; - + if (comparison < 0) return i; } - + return groupItems.Count; } @@ -1269,7 +1649,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient kvp.Key != insertedGroupKey && kvp.Value > insertedHeaderIndex) .Select(kvp => kvp.Key) @@ -1321,6 +1701,15 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient>(this); @@ -1342,6 +1731,8 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient +/// 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. +/// +public class ThrottledEventHandler : IDisposable +{ + private readonly int _delayMilliseconds; + private readonly Func _asyncCallback; + private readonly Action _syncCallback; + private Timer _timer; + private volatile bool _disposed; + + /// + /// Creates a new throttled event handler with a synchronous callback. + /// + /// The delay in milliseconds to wait before executing the callback + /// The action to execute after the delay period + public ThrottledEventHandler(int delayMilliseconds, Action callback) + { + _delayMilliseconds = delayMilliseconds; + _syncCallback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + /// + /// Creates a new throttled event handler with an asynchronous callback. + /// + /// The delay in milliseconds to wait before executing the callback + /// The async function to execute after the delay period + public ThrottledEventHandler(int delayMilliseconds, Func callback) + { + _delayMilliseconds = delayMilliseconds; + _asyncCallback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + /// + /// Triggers the throttled execution. If called again before the delay period expires, + /// the timer is reset and the callback execution is delayed further. + /// + 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; + } +} \ No newline at end of file diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index b0d0cbfd..87b90532 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -21,11 +19,13 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; +using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Services; using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Data; +using Wino.Mail.ViewModels.Helpers; using Wino.Mail.ViewModels.Messages; using Wino.Messaging.Client.Mails; using Wino.Messaging.Server; @@ -36,8 +36,6 @@ namespace Wino.Mail.ViewModels; public partial class MailListPageViewModel : MailBaseViewModel, IRecipient, IRecipient, - IRecipient, - IRecipient, IRecipient, IRecipient, IRecipient, @@ -57,10 +55,10 @@ public partial class MailListPageViewModel : MailBaseViewModel, private readonly HashSet gmailUnreadFolderMarkedAsReadUniqueIds = []; - private IObservable> selectionChangedObservable = null; + private ThrottledEventHandler _selectionChangedThrottler; public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection(); - public ObservableCollection SelectedItems { get; set; } = []; + //public ObservableCollection SelectedItems { get; set; } = []; public ObservableCollection PivotFolders { get; set; } = []; public ObservableCollection ActionItems { get; set; } = []; @@ -177,37 +175,56 @@ public partial class MailListPageViewModel : MailBaseViewModel, mailListLength = statePersistenceService.MailListPaneLength; - selectionChangedObservable = Observable.FromEventPattern(SelectedItems, nameof(SelectedItems.CollectionChanged)); - selectionChangedObservable - .Throttle(TimeSpan.FromMilliseconds(100)) - .Subscribe(async a => + _selectionChangedThrottler = new ThrottledEventHandler(100, () => + { + _ = ExecuteUIThread(() => { - await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); }); - }); + if (MailCollection.SelectedVisibleCount == 1) + { + ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0)); + } + else + { + // At this point, either we don't have any item selected + // or we have multiple item selected. In either case + // there should be no active item. - //MailCollection.MailItemRemoved += (c, removedItem) => - //{ - // if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem) - // { - // foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast()) - // { - // if (SelectedItems.Contains(viewModel)) - // { - // SelectedItems.Remove(viewModel); - // } - // } - // } - // else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel)) - // { - // SelectedItems.Remove(removedMailItemViewModel); - // } - //}; + ActiveMailItemChanged(null); + } + + NotifyItemSelected(); + SetupTopBarActions(); + }); + }); + } + + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + MailCollection.SelectionChanged += SelectedItemsChanged; + } + + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + base.OnNavigatedFrom(mode, parameters); + + MailCollection.SelectionChanged -= SelectedItemsChanged; + MailCollection.Dispose(); + + _selectionChangedThrottler?.Dispose(); + _selectionChangedThrottler = null; + } + + private void SelectedItemsChanged(object sender, EventArgs e) + { + _selectionChangedThrottler?.Trigger(); } private void SetupTopBarActions() { ActionItems.Clear(); - var actions = GetAvailableMailActions(SelectedItems); + var actions = GetAvailableMailActions(MailCollection.SelectedVisibleItems); actions.ForEach(a => ActionItems.Add(a)); } @@ -248,13 +265,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled; public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false; - public int SelectedItemCount => SelectedItems.Count; - public bool HasMultipleItemSelections => SelectedItemCount > 1; - public bool HasSingleItemSelection => SelectedItemCount == 1; - public bool HasSelectedItems => SelectedItems.Any(); public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive; - public string SelectedMessageText => HasSelectedItems ? string.Format(Translator.MailsSelected, SelectedItemCount) : Translator.NoMailSelected; + public string SelectedMessageText => MailCollection.SelectedVisibleCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedVisibleCount) : Translator.NoMailSelected; /// /// Indicates current state of the mail list. Doesn't matter it's loading or no. @@ -333,13 +346,12 @@ public partial class MailListPageViewModel : MailBaseViewModel, public void NotifyItemSelected() { OnPropertyChanged(nameof(SelectedMessageText)); - OnPropertyChanged(nameof(HasSingleItemSelection)); - OnPropertyChanged(nameof(HasSelectedItems)); - OnPropertyChanged(nameof(SelectedItemCount)); - OnPropertyChanged(nameof(HasMultipleItemSelections)); + //OnPropertyChanged(nameof(HasSingleItemSelection)); + //OnPropertyChanged(nameof(HasSelectedItems)); + //OnPropertyChanged(nameof(SelectedItemCount)); + //OnPropertyChanged(nameof(HasMultipleItemSelections)); - if (SelectedFolderPivot != null) - SelectedFolderPivot.SelectedItemCount = SelectedItemCount; + SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount; } private void NotifyItemFoundState() @@ -360,26 +372,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, }); } - private void SelectedItemCollectionUpdated(NotifyCollectionChangedEventArgs e) - { - if (SelectedItems.Count == 1) - { - ActiveMailItemChanged(SelectedItems[0]); - } - else - { - // At this point, either we don't have any item selected - // or we have multiple item selected. In either case - // there should be no active item. - - ActiveMailItemChanged(null); - } - - NotifyItemSelected(); - - SetupTopBarActions(); - } - private async Task UpdateFolderPivotsAsync() { if (ActiveFolder == null) return; @@ -440,9 +432,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, [RelayCommand] private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem) { - if (menuItem == null || !SelectedItems.Any()) return; + if (menuItem == null || MailCollection.SelectedVisibleCount == 0) return; - await HandleMailOperation(menuItem.Operation, SelectedItems); + await HandleMailOperation(menuItem.Operation, MailCollection.SelectedVisibleItems); } /// @@ -452,9 +444,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, [RelayCommand] private async Task ExecuteMailOperation(MailOperation mailOperation) { - if (!SelectedItems.Any()) return; + if (!MailCollection.SelectedVisibleItems.Any()) return; - await HandleMailOperation(mailOperation, SelectedItems); + await HandleMailOperation(mailOperation, MailCollection.SelectedVisibleItems); } private async Task HandleMailOperation(MailOperation mailOperation, IEnumerable mailItems) @@ -615,9 +607,11 @@ public partial class MailListPageViewModel : MailBaseViewModel, await listManipulationSemepahore.WaitAsync(); - // await MailCollection.AddAsync(addedMail); - - await ExecuteUIThread(() => { NotifyItemFoundState(); }); + await ExecuteUIThread(() => + { + MailCollection.AddEmail(new MailItemViewModel(addedMail)); + NotifyItemFoundState(); + }); } catch { } finally @@ -632,6 +626,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); + // TODO // await MailCollection.UpdateMailCopy(updatedMail); await ExecuteUIThread(() => { SetupTopBarActions(); }); @@ -658,7 +653,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction) { - bool isDeletedMailSelected = SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId); + bool isDeletedMailSelected = MailCollection.SelectedVisibleItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId); // Automatically select the next item in the list if the setting is enabled. MailItemViewModel nextItem = null; @@ -667,12 +662,15 @@ public partial class MailListPageViewModel : MailBaseViewModel, { await ExecuteUIThread(() => { - // nextItem = MailCollection.GetNextItem(removedMail); + nextItem = MailCollection.GetNextItem(removedMail); }); } // Remove the deleted item from the list. - // await MailCollection.RemoveAsync(removedMail); + await ExecuteUIThread(() => + { + MailCollection.RemoveEmailByMailCopy(removedMail); + }); if (nextItem != null) WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true)); @@ -681,7 +679,10 @@ public partial class MailListPageViewModel : MailBaseViewModel, // There are no next item to select, but we removed the last item which was selected. // Clearing selected item will dispose rendering page. - SelectedItems.Clear(); + await ExecuteUIThread(() => + { + MailCollection.ClearSelections(); + }); } await ExecuteUIThread(() => { NotifyItemFoundState(); }); @@ -743,8 +744,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, { MailCollection.Clear(); - SelectedItems.Clear(); - if (ActiveFolder == null) return; @@ -885,16 +884,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, #region Receivers - void IRecipient.Receive(MailItemSelectedEvent message) - { - if (!SelectedItems.Contains(message.SelectedMailItem)) SelectedItems.Add(message.SelectedMailItem); - } - - void IRecipient.Receive(MailItemSelectionRemovedEvent message) - { - if (SelectedItems.Contains(message.RemovedMailItem)) SelectedItems.Remove(message.RemovedMailItem); - } - async void IRecipient.Receive(ActiveMailFolderChangedEvent message) { NotifyItemSelected(); @@ -993,16 +982,15 @@ public partial class MailListPageViewModel : MailBaseViewModel, for (int i = 0; i < 3; i++) { - // TODO: Get container. - //var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId); + var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId); - //if (mailContainer != null) - //{ - // navigatingMailItem = mailContainer.ItemViewModel; - // threadMailItemViewModel = mailContainer.ThreadViewModel; + if (mailContainer != null) + { + navigatingMailItem = mailContainer.ItemViewModel; + threadMailItemViewModel = mailContainer.ThreadViewModel; - // break; - //} + break; + } } if (threadMailItemViewModel != null) @@ -1079,7 +1067,10 @@ public partial class MailListPageViewModel : MailBaseViewModel, public void Receive(ThumbnailAdded message) { - // MailCollection.UpdateThumbnails(message.Email); + Dispatcher.ExecuteOnUIThread(() => + { + MailCollection.UpdateThumbnailsForAddress(message.Email); + }); } protected override void RegisterRecipients() @@ -1088,8 +1079,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, Messenger.Register(this); Messenger.Register(this); - Messenger.Register(this); - Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); @@ -1103,8 +1092,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, Messenger.Unregister(this); Messenger.Unregister(this); - Messenger.Unregister(this); - Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); diff --git a/Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs b/Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs deleted file mode 100644 index b30b6a36..00000000 --- a/Wino.Mail.ViewModels/Messages/MailItemSelectedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Wino.Mail.ViewModels.Data; - -namespace Wino.Mail.ViewModels.Messages; - -/// -/// Wino has complex selected item detection mechanism with nested ListViews that -/// supports multi selection with threads. Each list view will raise this for mail list page -/// to react. -/// -public class MailItemSelectedEvent -{ - public MailItemSelectedEvent(MailItemViewModel selectedMailItem) - { - SelectedMailItem = selectedMailItem; - } - - public MailItemViewModel SelectedMailItem { get; set; } -} diff --git a/Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs b/Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs deleted file mode 100644 index d098ba0f..00000000 --- a/Wino.Mail.ViewModels/Messages/MailItemSelectionRemovedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Wino.Mail.ViewModels.Data; - -namespace Wino.Mail.ViewModels.Messages; - -/// -/// Selected item removed event. -/// -public class MailItemSelectionRemovedEvent -{ - public MailItemSelectionRemovedEvent(MailItemViewModel removedMailItem) - { - RemovedMailItem = removedMailItem; - } - - public MailItemViewModel RemovedMailItem { get; set; } -} diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 8fff4f55..a803db7c 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -104,10 +104,11 @@ public partial class App : WinoApplication, IRecipient "b19c2035-d740-49ff-b297-de6ec561b208"; - public string[] OutlookScope => new string[] - { + public string[] OutlookScope => + [ "email", "mail.readwrite", "offline_access", @@ -15,17 +15,17 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig "Mail.Send.Shared", "Mail.ReadWrite.Shared", "User.Read" - }; + ]; public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; - public string[] GmailScope => new string[] - { + public string[] GmailScope => + [ "https://mail.google.com/", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/gmail.labels", "https://www.googleapis.com/auth/userinfo.email" - }; + ]; public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore"; } diff --git a/Wino.Mail.WinUI/Views/ComposePage.xaml.cs b/Wino.Mail.WinUI/Views/ComposePage.xaml.cs index 241d7244..d519bab1 100644 --- a/Wino.Mail.WinUI/Views/ComposePage.xaml.cs +++ b/Wino.Mail.WinUI/Views/ComposePage.xaml.cs @@ -10,6 +10,7 @@ using EmailValidation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Navigation; using MimeKit; using Windows.ApplicationModel.DataTransfer; @@ -34,12 +35,10 @@ public sealed partial class ComposePage : ComposePageAbstract, public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView(); private readonly List _disposables = []; - private readonly SystemNavigationManagerPreview _navManagerPreview = SystemNavigationManagerPreview.GetForCurrentView(); public ComposePage() { InitializeComponent(); - _navManagerPreview.CloseRequested += OnClose; } private async void GlobalFocusManagerGotFocus(object sender, FocusManagerGotFocusEventArgs e) @@ -235,9 +234,8 @@ public sealed partial class ComposePage : ComposePageAbstract, FocusManager.GotFocus += GlobalFocusManagerGotFocus; - // TODO: disabled animation for now, since it's still not working properly. - //var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation"); - //anim?.TryStart(GetWebView()); + var anim = ConnectedAnimationService.GetForCurrentView().GetAnimation("WebViewConnectedAnimation"); + anim?.TryStart(GetWebView()); _disposables.Add(GetSuggestionBoxDisposable(ToBox)); _disposables.Add(GetSuggestionBoxDisposable(CCBox)); @@ -371,7 +369,6 @@ public sealed partial class ComposePage : ComposePageAbstract, base.OnNavigatingFrom(e); FocusManager.GotFocus -= GlobalFocusManagerGotFocus; - _navManagerPreview.CloseRequested -= OnClose; await ViewModel.UpdateMimeChangesAsync(); DisposeDisposables(); diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml b/Wino.Mail.WinUI/Views/MailListPage.xaml index 72f52bb9..0566310b 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml @@ -33,6 +33,7 @@ mc:Ignorable="d"> + 1 0,0,0,0 0,0,12,0 @@ -58,6 +59,7 @@ @@ -129,12 +131,16 @@ - + @@ -251,7 +257,7 @@ @@ -436,10 +442,9 @@ ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}" Layout="{StaticResource DefaultItemsViewLayout}" LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}" - SelectionChanged="ListSelectionChanged" + ProcessKeyboardAccelerators="MailListView_ProcessKeyboardAccelerators" SelectionMode="Extended" /> -