From d02205fba3d6bc740e43e157de1f8fa0f3a56187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Tue, 28 Oct 2025 14:43:22 +0100 Subject: [PATCH] Item vm prop changes. --- Wino.Core.Domain/Enums/EmailGroupingType.cs | 10 + .../Collections/GroupedEmailCollection.cs | 1822 ----------------- .../Collections/WinoMailCollection.cs | 4 +- Wino.Mail.ViewModels/ComposePageViewModel.cs | 3 +- .../Data/MailItemViewModel.cs | 95 +- .../Data/ThreadMailItemViewModel.cs | 148 +- Wino.Mail.WinUI/AppShell.xaml.cs | 4 +- .../MailItemDisplayInformationControl.xaml | 31 +- .../MailItemDisplayInformationControl.xaml.cs | 186 +- Wino.Mail.WinUI/Views/MailListPage.xaml | 30 +- .../Views/Settings/PersonalizationPage.xaml | 18 +- 11 files changed, 346 insertions(+), 2005 deletions(-) create mode 100644 Wino.Core.Domain/Enums/EmailGroupingType.cs delete mode 100644 Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs diff --git a/Wino.Core.Domain/Enums/EmailGroupingType.cs b/Wino.Core.Domain/Enums/EmailGroupingType.cs new file mode 100644 index 00000000..703cc21a --- /dev/null +++ b/Wino.Core.Domain/Enums/EmailGroupingType.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Grouping options for emails +/// +public enum EmailGroupingType +{ + ByFromName, + ByDate +} diff --git a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs deleted file mode 100644 index 58865e9a..00000000 --- a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs +++ /dev/null @@ -1,1822 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -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; - -/// -/// Grouping options for emails -/// -public enum EmailGroupingType -{ - ByFromName, - ByDate -} - -/// -/// Sorting options for emails within groups -/// -public enum EmailSortDirection -{ - Ascending, - Descending -} - -/// -/// Collection that automatically groups MailItemViewModels with ThreadMailItemViewModels in a flat structure for ItemsView. -/// All emails are in the same flat list with proper selection support. Thread emails are placed consecutively after their expander. -/// -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; - - [ObservableProperty] - private EmailGroupingType groupingType = EmailGroupingType.ByDate; - - [ObservableProperty] - private EmailSortDirection sortDirection = EmailSortDirection.Descending; - - // Tracks the number of currently selected visible mail items. Notify derived bools when changed. - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasSelectedItems))] - [NotifyPropertyChangedFor(nameof(HasSingleItemSelected))] - [NotifyPropertyChangedFor(nameof(HasMultipleItemsSelected))] - [NotifyPropertyChangedFor(nameof(IsAllItemsSelected))] - public partial int SelectedVisibleCount { get; set; } - - /// - /// 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; - - /// - /// Indicates whether all mail items are currently selected. - /// Counts all mail items including those in threads, regardless of thread expansion state. - /// - public bool IsAllItemsSelected - { - get - { - var totalMailItems = _sourceItems.Count; - if (totalMailItems == 0) return false; - - var selectedCount = 0; - - // Count selected standalone emails (not in threads) - selectedCount += _sourceItems.Count(e => e.IsSelected && !e.IsDisplayedInThread); - - // Count selected emails in threads - foreach (var expander in _threadExpanders.Values) - { - if (expander.IsSelected) - { - // If thread is selected, all emails in the thread are considered selected - selectedCount += expander.ThreadEmails.Count; - } - else - { - // If thread is not selected, count only individually selected emails within the thread - selectedCount += expander.ThreadEmails.Count(e => e.IsSelected); - } - } - - return selectedCount == totalMailItems; - } - } - - public GroupedEmailCollection() - { - // Create a flat collection for ItemsView with headers, expanders and emails mixed - Items = []; - - // Subscribe to source collection changes to update grouping - _sourceItems.CollectionChanged += OnSourceItemsChanged; - - // Register for PropertyChanged messages - WeakReferenceMessenger.Default.Register>(this); - - RefreshGrouping(); - } - - /// - /// Flat observable collection containing group headers, thread expanders, and email items for ItemsView binding. - /// Structure: GroupHeader -> [ThreadExpander -> ThreadEmail1, ThreadEmail2, ...] -> StandaloneEmail1 -> StandaloneEmail2 - /// - public ObservableCollection Items { get; } - - /// - /// Total number of emails across all groups - /// - public int TotalCount => _sourceItems.Count; - - /// - /// Total number of unread emails across all groups - /// - public int TotalUnreadCount => _sourceItems.Count(e => e.MailCopy?.IsRead == false); - - /// - /// HashSet containing unique IDs of all mail copies in the collection for pagination tracking - /// - public HashSet MailCopyIdHashSet => _mailCopyIdHashSet; - - /// - /// Gets all email items across all groups as a flat collection - /// - 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: - /// - Standalone mail items where IsSelected=true - /// - Mail items inside threads where the mail item's IsSelected=true (regardless of thread expansion) - /// - All mail items inside a thread where the thread's IsSelected=true - /// - public IEnumerable SelectedItems - { - get - { - var selectedItems = new List(); - - // Add selected standalone emails (not in threads) - selectedItems.AddRange(_sourceItems.Where(e => e.IsSelected && !e.IsDisplayedInThread)); - - // Process thread expanders - foreach (var expander in _threadExpanders.Values) - { - if (expander.IsSelected) - { - // If thread is selected, add all emails in the thread - selectedItems.AddRange(expander.ThreadEmails); - } - else - { - // If thread is not selected, add only individually selected emails within the thread - selectedItems.AddRange(expander.ThreadEmails.Where(e => e.IsSelected)); - } - } - - return selectedItems; - } - } - - /// - /// Gets the count of all currently selected email items. - /// Counts: - /// - Standalone mail items where IsSelected=true - /// - Mail items inside threads where the mail item's IsSelected=true (regardless of thread expansion) - /// - All mail items inside a thread where the thread's IsSelected=true - /// - public int SelectedItemsCount => SelectedItems.Count(); - - /// - /// Gets the number of visible email items (excluding group headers). - /// For threads, counts the expander as 1 if collapsed, or all thread emails if expanded. - /// - public int Count - { - get - { - int count = 0; - - foreach (var item in Items) - { - switch (item) - { - case GroupHeaderBase: - // Skip group headers - break; - case ThreadMailItemViewModel thread: - count += thread.ThreadEmails.Count; - break; - case MailItemViewModel: - count += 1; - break; - } - } - return count; - } - } - - /// - /// Handles PropertyChanged messages for thread expansion and mail item selection - /// - public void Receive(PropertyChangedMessage message) - { - // Only handle IsThreadExpanded property changes from ThreadMailItemViewModel - if (_isUpdating) - return; - - if (message.PropertyName == nameof(ThreadMailItemViewModel.IsThreadExpanded) && message.Sender is ThreadMailItemViewModel expander) - { - HandleThreadExpansion(expander); - } - else if (message.PropertyName == nameof(MailItemViewModel.IsSelected) && message.Sender is MailItemViewModel mailItem) - { - HandleMailItemSelectionChanged(mailItem, message.NewValue); - } - else if (message.PropertyName == nameof(ThreadMailItemViewModel.IsSelected) && message.Sender is ThreadMailItemViewModel threadExpander) - { - HandleThreadSelectionChanged(threadExpander, message.NewValue); - } - } - - private void HandleMailItemSelectionChanged(MailItemViewModel mailItem, bool isSelected) - { - bool selectionChanged = false; - - if (isSelected) - { - // Add to selected items if it's visible in the UI - if (Items.Contains(mailItem)) - { - if (_selectedVisibleItems.Add(mailItem)) - { - SelectedVisibleCount = _selectedVisibleItems.Count; - OnPropertyChanged(nameof(SelectedVisibleItems)); - selectionChanged = true; - } - } - } - else - { - // Remove from selected items - if (_selectedVisibleItems.Remove(mailItem)) - { - SelectedVisibleCount = _selectedVisibleItems.Count; - OnPropertyChanged(nameof(SelectedVisibleItems)); - selectionChanged = true; - } - } - - if (selectionChanged) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - } - - private void HandleThreadSelectionChanged(ThreadMailItemViewModel threadExpander, bool isSelected) - { - // When a thread expander's selection changes, it affects the selection state of all emails in that thread - // We need to notify that the "all items selected" state might have changed - OnPropertyChanged(nameof(IsAllItemsSelected)); - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - - /// - /// 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) - { - _isUpdating = true; - try - { - var expanderIndex = Items.IndexOf(expander); - if (expanderIndex == -1) - return; - - if (expander.IsThreadExpanded) - { - // Add thread emails after the expander - var insertIndex = expanderIndex + 1; - var sortedThreadEmails = SortDirection == EmailSortDirection.Descending - ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy.CreationDate).ToList() - : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); - - foreach (var email in sortedThreadEmails) - { - Items.Insert(insertIndex, email); - RegisterMailItemForSelectionTracking(email); - insertIndex++; - } - - UpdateHeaderIndicesAfterInsertion(expanderIndex + 1, expander.EmailCount); - } - else - { - // Remove thread emails from UI - foreach (var email in expander.ThreadEmails.ToList()) - { - var emailIndex = Items.IndexOf(email); - if (emailIndex >= 0) - { - UnregisterMailItemFromSelectionTracking(email); - Items.RemoveAt(emailIndex); - UpdateHeaderIndicesAfterRemoval(emailIndex); - } - } - } - - // Notify that the "all items selected" state might have changed due to visible item count change - OnPropertyChanged(nameof(IsAllItemsSelected)); - } - finally - { - _isUpdating = false; - } - } - - /// - /// Adds an email to the collection which will automatically group it. - /// If an email with the same ThreadId exists, adds it to the existing thread or creates a new thread. - /// - public void AddEmail(MailItemViewModel email) - { - if (email?.MailCopy == null) - return; - - // Add to unique ID tracking - _mailCopyIdHashSet.Add(email.MailCopy.UniqueId); - - _isUpdating = true; - try - { - // Check if this email belongs to a thread - if (!string.IsNullOrEmpty(email.MailCopy.ThreadId)) - { - // Look for existing emails with the same ThreadId - var existingThreadEmails = _sourceItems - .Where(e => e.MailCopy?.ThreadId == email.MailCopy.ThreadId) - .ToList(); - - var existingExpander = _threadExpanders.GetValueOrDefault(email.MailCopy.ThreadId); - - if (existingThreadEmails.Any() || existingExpander != null) - { - // Add to existing thread - if (existingExpander == null) - { - // Create thread expander for the first time (existing emails become part of thread) - existingExpander = new ThreadMailItemViewModel(email.MailCopy.ThreadId); - _threadExpanders[email.MailCopy.ThreadId] = existingExpander; - - // Remove existing standalone emails from UI and add them to the thread - foreach (var existingEmail in existingThreadEmails) - { - RemoveEmailFromUI(existingEmail); - existingExpander.AddEmail(existingEmail); - existingEmail.IsDisplayedInThread = true; - } - } - - // Add the new email to the thread - existingExpander.AddEmail(email); - email.IsDisplayedInThread = true; - - // Add to source collection - var insertIndex = FindInsertionIndex(email); - _sourceItems.Insert(insertIndex, email); - - // Add thread expander and all emails to UI in correct positions - RefreshThreadInUI(existingExpander); - } - else - { - // First email with this ThreadId - treat as standalone for now - email.IsDisplayedInThread = false; - var insertIndex = FindInsertionIndex(email); - _sourceItems.Insert(insertIndex, email); - AddEmailToUI(email); - } - } - else - { - // No ThreadId - standalone email - email.IsDisplayedInThread = false; - var insertIndex = FindInsertionIndex(email); - _sourceItems.Insert(insertIndex, email); - AddEmailToUI(email); - } - - OnPropertyChanged(nameof(TotalCount)); - OnPropertyChanged(nameof(TotalUnreadCount)); - } - finally - { - _isUpdating = false; - } - } - - /// - /// Removes an email from the collection. - /// If the email is part of a thread and removing it would leave only 1 item in the thread, - /// the thread is converted back to a single email. - /// - public void RemoveEmail(MailItemViewModel email) - { - if (email?.MailCopy == null) - return; - - RemoveEmailByMailCopy(email.MailCopy); - } - - /// - /// 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(mailCopy.UniqueId); - - _isUpdating = true; - try - { - // 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 - _sourceItems.Remove(email); - - if (!string.IsNullOrEmpty(threadId) && _threadExpanders.TryGetValue(threadId, out var expander)) - { - // Remove from thread - expander.RemoveEmail(email); - email.IsDisplayedInThread = false; - - // Remove email from UI if it's visible (only if thread is expanded) - RemoveEmailFromUI(email); - - // Check if thread now has only 1 email - convert back to standalone - if (expander.EmailCount == 1) - { - var remainingEmail = expander.ThreadEmails.First(); - - // Remove thread expander and remaining email from UI - RemoveThreadFromUI(expander); - - // Set remaining email as no longer displayed in thread - remainingEmail.IsDisplayedInThread = false; - - // Remove thread expander tracking - _threadExpanders.Remove(threadId); - expander.Dispose(); - - // Add remaining email as standalone - AddEmailToUI(remainingEmail); - } - else if (expander.EmailCount == 0) - { - // Thread is empty - remove completely - RemoveThreadFromUI(expander); - _threadExpanders.Remove(threadId); - expander.Dispose(); - } - else - { - // Thread still has multiple emails - refresh its position - RefreshThreadInUI(expander); - } - } - else - { - // Standalone email - email.IsDisplayedInThread = false; - RemoveEmailFromUI(email); - } - - // Update group headers - UpdateGroupAfterChanges(); - - OnPropertyChanged(nameof(TotalCount)); - OnPropertyChanged(nameof(TotalUnreadCount)); - } - finally - { - _isUpdating = false; - } - } - - /// - /// Adds multiple emails to the collection efficiently using bulk operations - /// - public void AddEmails(IEnumerable emails) - { - var emailList = emails.Where(e => e?.MailCopy != null).ToList(); - if (!emailList.Any()) - return; - - _isUpdating = true; - try - { - // Add to unique ID tracking - foreach (var email in emailList) - { - _mailCopyIdHashSet.Add(email.MailCopy.UniqueId); - } - - // For bulk loading, add to source and use incremental refresh to preserve selection - foreach (var email in emailList) - { - var insertIndex = FindInsertionIndex(email); - _sourceItems.Insert(insertIndex, email); - } - - // Use incremental refresh instead of full refresh to preserve selection - IncrementalRefreshGrouping(emailList); - - OnPropertyChanged(nameof(TotalCount)); - OnPropertyChanged(nameof(TotalUnreadCount)); - } - finally - { - _isUpdating = false; - } - } - - /// - /// Clears all emails, threads, and headers - /// - public void Clear() - { - _isUpdating = true; - try - { - // Unregister all mail items from selection tracking - foreach (var item in Items) - { - if (item is MailItemViewModel mailItem) - { - UnregisterMailItemFromSelectionTracking(mailItem); - } - } - - // Clear selected items tracking - var hadSelectedItems = _selectedVisibleItems.Count > 0; - _selectedVisibleItems.Clear(); - SelectedVisibleCount = 0; - - // Reset IsDisplayedInThread for all emails before clearing - foreach (var email in _sourceItems) - { - email.IsDisplayedInThread = false; - } - - // Dispose all thread expanders - foreach (var expander in _threadExpanders.Values) - { - expander.Dispose(); - } - - _sourceItems.Clear(); - Items.Clear(); - _groupHeaders.Clear(); - _groupHeaderIndexCache.Clear(); - _groupItems.Clear(); - _threadExpanders.Clear(); - _mailCopyIdHashSet.Clear(); - - OnPropertyChanged(nameof(TotalCount)); - OnPropertyChanged(nameof(TotalUnreadCount)); - OnPropertyChanged(nameof(SelectedVisibleItems)); - OnPropertyChanged(nameof(IsAllItemsSelected)); - - if (hadSelectedItems) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - } - finally - { - _isUpdating = false; - } - } - - /// - /// Changes the grouping type and rebuilds the collection - /// - public void ChangeGrouping(EmailGroupingType newGroupingType, EmailSortDirection newSortDirection = EmailSortDirection.Descending) - { - if (GroupingType == newGroupingType && SortDirection == newSortDirection) - return; - - GroupingType = newGroupingType; - SortDirection = newSortDirection; - RefreshGrouping(); - } - - /// - /// Manually refreshes the grouping (useful after bulk operations) - /// - public void RefreshGrouping() - { - _isUpdating = true; - try - { - // Unregister all mail items before clearing - foreach (var item in Items) - { - if (item is MailItemViewModel mailItem) - { - UnregisterMailItemFromSelectionTracking(mailItem); - } - } - - // Clear UI items but preserve source and expanders - Items.Clear(); - _groupHeaders.Clear(); - _groupHeaderIndexCache.Clear(); - _groupItems.Clear(); - var hadSelectedItems = _selectedVisibleItems.Count > 0; - _selectedVisibleItems.Clear(); - SelectedVisibleCount = 0; - - if (!_sourceItems.Any()) - { - if (hadSelectedItems) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - return; - } - - // Rebuild thread expanders based on current emails - RebuildThreadExpanders(); - - // Group all items (standalone emails and thread expanders) by criteria - var allItems = new List(); - - // Add standalone emails (emails without threads or not in any expander) - var standaloneEmails = _sourceItems - .Where(e => string.IsNullOrEmpty(e.MailCopy?.ThreadId) || - !_threadExpanders.ContainsKey(e.MailCopy.ThreadId)) - .ToList(); - - allItems.AddRange(standaloneEmails.Cast()); - allItems.AddRange(_threadExpanders.Values.Cast()); - - // Group by criteria - var groupedItems = allItems - .GroupBy(item => GetGroupKeyForItem(item)) - .OrderBy(g => g.Key, GetGroupComparer()); - - var currentIndex = 0; - - // Process each group - foreach (var group in groupedItems) - { - // Create group header - var groupHeader = CreateGroupHeader(group.Key); - _groupHeaders[group.Key] = groupHeader; - _groupHeaderIndexCache[group.Key] = currentIndex; - - // Sort items within the group - var sortedGroupItems = SortDirection == EmailSortDirection.Descending - ? group.OrderByDescending(GetEffectiveDate).ToList() - : group.OrderBy(GetEffectiveDate).ToList(); - - _groupItems[group.Key] = sortedGroupItems; - - // Add header to flat collection - Items.Add(groupHeader); - currentIndex++; - - // Add all items in this group to flat collection - foreach (var item in sortedGroupItems) - { - if (item is ThreadMailItemViewModel expander) - { - // Add expander - Items.Add(expander); - currentIndex++; - - // Only add thread emails if the thread is expanded - if (expander.IsThreadExpanded) - { - var sortedThreadEmails = SortDirection == EmailSortDirection.Descending - ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() - : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); - - foreach (var threadEmail in sortedThreadEmails) - { - Items.Add(threadEmail); - RegisterMailItemForSelectionTracking(threadEmail); - currentIndex++; - } - } - } - else if (item is MailItemViewModel email) - { - // Add standalone email - Items.Add(email); - RegisterMailItemForSelectionTracking(email); - currentIndex++; - } - } - } - - // Update group header counts - UpdateAllGroupHeaderCounts(); - - // Notify that the "all items selected" state might have changed due to visible items rebuild - OnPropertyChanged(nameof(IsAllItemsSelected)); - - if (hadSelectedItems) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - } - finally - { - _isUpdating = false; - } - } - - /// - /// Incrementally adds new emails to the collection without clearing existing items - /// - private void IncrementalRefreshGrouping(IList newEmails) - { - _isUpdating = true; - try - { - if (!newEmails.Any()) - return; - - // Update thread expanders with any new emails that should be threaded - UpdateThreadExpandersForNewEmails(newEmails); - - // Process each new email - foreach (var email in newEmails) - { - // Skip if it's already displayed in a thread - if (email.IsDisplayedInThread) - continue; - - // Determine the group key for this email - var groupKey = GetGroupKeyForItem(email); - - // Get or create the group header - var groupHeader = GetOrCreateGroupHeader(groupKey); - - // Find where this email should be inserted in the UI - var insertPosition = FindUIInsertionPosition(email, groupKey); - - // Insert the email at the correct position - Items.Insert(insertPosition, email); - RegisterMailItemForSelectionTracking(email); - - // Update the group items list - if (!_groupItems.ContainsKey(groupKey)) - { - _groupItems[groupKey] = new List(); - } - - // 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) - { - // Find where to insert the header - var headerInsertPosition = FindHeaderInsertionPosition(groupKey, groupHeader); - Items.Insert(headerInsertPosition, groupHeader); - _groupHeaderIndexCache[groupKey] = headerInsertPosition; - - // Update all subsequent header indices - UpdateSubsequentHeaderIndices(groupKey, 1); - } - } - - // Update group header counts for affected groups - UpdateGroupHeaderCountsForNewEmails(newEmails); - } - finally - { - _isUpdating = false; - } - } - - /// - /// Rebuilds thread expanders based on current source emails - /// - private void RebuildThreadExpanders() - { - // Group emails by ThreadId - var threadGroups = _sourceItems - .Where(e => !string.IsNullOrEmpty(e.MailCopy?.ThreadId)) - .GroupBy(e => e.MailCopy!.ThreadId!) - .Where(g => g.Count() >= 2) // Only create threads with 2+ emails - .ToList(); - - // Remove expanders for threads that no longer have 2+ emails - var expandersToRemove = _threadExpanders.Keys - .Where(threadId => !threadGroups.Any(g => g.Key == threadId)) - .ToList(); - - foreach (var threadId in expandersToRemove) - { - // Set emails back to not displayed in thread before removing expander - foreach (var email in _threadExpanders[threadId].ThreadEmails) - { - email.IsDisplayedInThread = false; - } - - _threadExpanders[threadId].Dispose(); - _threadExpanders.Remove(threadId); - } - - // Create or update expanders for threads with 2+ emails - foreach (var threadGroup in threadGroups) - { - if (!_threadExpanders.TryGetValue(threadGroup.Key, out var threadExpander)) - { - threadExpander = new ThreadMailItemViewModel(threadGroup.Key); - _threadExpanders[threadGroup.Key] = threadExpander; - } - - // Clear and re-add emails to ensure consistency - var currentEmails = threadExpander.ThreadEmails.ToList(); - foreach (var email in currentEmails) - { - threadExpander.RemoveEmail(email); - email.IsDisplayedInThread = false; - } - - foreach (var email in threadGroup) - { - threadExpander.AddEmail(email); - email.IsDisplayedInThread = true; - } - } - - // Set standalone emails to not displayed in thread - var standaloneEmails = _sourceItems - .Where(e => string.IsNullOrEmpty(e.MailCopy?.ThreadId) || - !_threadExpanders.ContainsKey(e.MailCopy.ThreadId)) - .ToList(); - - foreach (var email in standaloneEmails) - { - email.IsDisplayedInThread = false; - } - } - - public int IndexOf(object item) => Items.IndexOf(item); - - private void RefreshThreadInUI(ThreadMailItemViewModel expander) - { - // Remove thread completely from UI - RemoveThreadFromUI(expander); - - // Find correct position for thread expander based on latest email - var groupKey = GetGroupKeyForItem(expander); - AddThreadToUI(expander, groupKey); - } - - public MailItemContainer GetMailItemContainer(Guid uniqueId) - { - // First, search in standalone mail items (not displayed in threads) - var standaloneMailItem = _sourceItems.FirstOrDefault(item => - item.MailCopy.UniqueId == uniqueId && !item.IsDisplayedInThread); - - if (standaloneMailItem != null) - { - // Check if the standalone item is visible in the UI - var isItemVisible = Items.Contains(standaloneMailItem); - - return new MailItemContainer(standaloneMailItem) - { - IsItemVisible = isItemVisible, - IsThreadVisible = false // Not a threaded item - }; - } - - // Search in thread expanders for threaded mail items - foreach (var threadExpander in _threadExpanders.Values) - { - if (threadExpander.HasUniqueId(uniqueId)) - { - // Find the specific mail item within the thread - var threadMailItem = threadExpander.ThreadEmails.FirstOrDefault(email => - email.MailCopy.UniqueId == uniqueId); - - if (threadMailItem != null) - { - // Check visibility: thread expander must be visible, and for individual item visibility, - // the thread must be expanded and the item must be in the visible Items collection - var isThreadVisible = Items.Contains(threadExpander); - var isItemVisible = isThreadVisible && threadExpander.IsThreadExpanded && Items.Contains(threadMailItem); - - return new MailItemContainer(threadMailItem, threadExpander) - { - IsItemVisible = isItemVisible, - IsThreadVisible = isThreadVisible - }; - } - } - } - - // Item not found - return null; - } - - /// - /// 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 mail items in the collection. - /// Includes standalone mail items and all mail items inside threads, regardless of thread expansion state. - /// - /// The number of items that were selected. - public int SelectAll() - { - var initialSelectedCount = SelectedItems.Count(); - - // Select all standalone emails (not in threads) - foreach (var mailItem in _sourceItems.Where(e => !e.IsDisplayedInThread)) - { - mailItem.IsSelected = true; - } - - // Select all thread expanders (which automatically selects all emails within them) - foreach (var expander in _threadExpanders.Values) - { - expander.IsSelected = true; - } - - SelectedVisibleCount = _selectedVisibleItems.Count; - - var finalSelectedCount = SelectedItems.Count(); - var selectedCount = finalSelectedCount - initialSelectedCount; - - if (selectedCount > 0) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - - return selectedCount; - } - - /// - /// Clears the selection of all mail items in the collection. - /// Includes standalone mail items and all mail items inside threads, regardless of thread expansion state. - /// - /// The number of items that were deselected. - public int ClearSelections() - { - var initialSelectedCount = SelectedItems.Count(); - - // Deselect all standalone emails (not in threads) - foreach (var mailItem in _sourceItems.Where(e => !e.IsDisplayedInThread)) - { - mailItem.IsSelected = false; - } - - // Deselect all thread expanders and individual emails in threads - foreach (var expander in _threadExpanders.Values) - { - expander.IsSelected = false; - - // Also explicitly deselect individual emails within threads - foreach (var threadEmail in expander.ThreadEmails) - { - threadEmail.IsSelected = false; - } - } - - SelectedVisibleCount = _selectedVisibleItems.Count; - - var finalSelectedCount = SelectedItems.Count(); - var deselectedCount = initialSelectedCount - finalSelectedCount; - - if (deselectedCount > 0) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - - return deselectedCount; - } - - private void AddThreadToUI(ThreadMailItemViewModel expander, string groupKey) - { - var groupHeader = GetOrCreateGroupHeader(groupKey); - var headerIndex = _groupHeaderIndexCache.GetValueOrDefault(groupKey, -1); - - if (headerIndex == -1) - { - // New group - add header, expander, and thread emails (if expanded) - var insertPosition = FindGroupInsertionPosition(groupKey); - - Items.Insert(insertPosition, groupHeader); - Items.Insert(insertPosition + 1, expander); - - var currentIndex = insertPosition + 2; - var totalInserted = 2 // header + expander - ; - - // Only add thread emails if the thread is expanded - if (expander.IsThreadExpanded) - { - var sortedThreadEmails = SortDirection == EmailSortDirection.Descending - ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() - : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); - - foreach (var email in sortedThreadEmails) - { - Items.Insert(currentIndex, email); - RegisterMailItemForSelectionTracking(email); - currentIndex++; - totalInserted++; - } - } - - UpdateHeaderIndicesAfterInsertion(insertPosition, totalInserted); - _groupHeaderIndexCache[groupKey] = insertPosition; - } - else - { - // Existing group - find correct position within group - var groupEndIndex = FindGroupEndIndex(headerIndex); - var insertIndex = FindItemInsertionIndexInGroup(expander, headerIndex, groupEndIndex); - - // Insert expander - Items.Insert(insertIndex, expander); - var currentIndex = insertIndex + 1; - var totalInserted = 1 // expander - ; - - // Only insert thread emails if expanded - if (expander.IsThreadExpanded) - { - var sortedThreadEmails = SortDirection == EmailSortDirection.Descending - ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() - : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); - - foreach (var email in sortedThreadEmails) - { - Items.Insert(currentIndex, email); - RegisterMailItemForSelectionTracking(email); - currentIndex++; - totalInserted++; - } - } - - UpdateHeaderIndicesAfterInsertion(insertIndex, totalInserted); - } - - UpdateGroupHeaderCounts(groupKey, groupHeader); - } - - private void RemoveThreadFromUI(ThreadMailItemViewModel expander) - { - // Remove expander - var expanderIndex = Items.IndexOf(expander); - if (expanderIndex >= 0) - { - Items.RemoveAt(expanderIndex); - UpdateHeaderIndicesAfterRemoval(expanderIndex); - } - - // Remove all thread emails (whether expanded or not) - foreach (var email in expander.ThreadEmails.ToList()) - { - var emailIndex = Items.IndexOf(email); - if (emailIndex >= 0) - { - UnregisterMailItemFromSelectionTracking(email); - Items.RemoveAt(emailIndex); - UpdateHeaderIndicesAfterRemoval(emailIndex); - } - } - } - - private void RemoveEmailFromUI(MailItemViewModel email) - { - var itemIndex = Items.IndexOf(email); - if (itemIndex >= 0) - { - UnregisterMailItemFromSelectionTracking(email); - Items.RemoveAt(itemIndex); - UpdateHeaderIndicesAfterRemoval(itemIndex); - } - } - - private void AddEmailToUI(MailItemViewModel email) - { - var groupKey = GetGroupKey(email); - var groupHeader = GetOrCreateGroupHeader(groupKey); - var headerIndex = _groupHeaderIndexCache.GetValueOrDefault(groupKey, -1); - - if (headerIndex == -1) - { - // New group - var insertPosition = FindGroupInsertionPosition(groupKey); - Items.Insert(insertPosition, groupHeader); - Items.Insert(insertPosition + 1, email); - RegisterMailItemForSelectionTracking(email); - - UpdateHeaderIndicesAfterInsertion(insertPosition, 2); - _groupHeaderIndexCache[groupKey] = insertPosition; - } - else - { - // Existing group - var groupEndIndex = FindGroupEndIndex(headerIndex); - var insertIndex = FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex); - Items.Insert(insertIndex, email); - RegisterMailItemForSelectionTracking(email); - - UpdateHeaderIndicesAfterInsertion(insertIndex); - } - - UpdateGroupHeaderCounts(groupKey, groupHeader); - } - - #region Helper Methods - - private string GetGroupKeyForItem(object item) - { - return item switch - { - MailItemViewModel email => GetGroupKey(email), - ThreadMailItemViewModel expander => GetGroupKey(expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).First()), - _ => "Default" - }; - } - - private string GetGroupKey(MailItemViewModel email) - { - return GroupingType switch - { - EmailGroupingType.ByFromName => email.FromName ?? "Unknown Sender", - EmailGroupingType.ByDate => email.MailCopy?.CreationDate.ToString("yyyy-MM-dd") ?? DateTime.Today.ToString("yyyy-MM-dd"), - _ => "Default" - }; - } - - private DateTime GetEffectiveDate(object item) - { - return item switch - { - MailItemViewModel email => email.MailCopy?.CreationDate ?? DateTime.MinValue, - ThreadMailItemViewModel expander => expander.LatestMailViewModel?.CreationDate ?? DateTime.MinValue, - _ => DateTime.MinValue - }; - } - - private int FindInsertionIndex(MailItemViewModel email) - { - var createdAt = email.MailCopy!.CreationDate; - int left = 0, right = _sourceItems.Count; - - while (left < right) - { - int mid = (left + right) / 2; - var comparison = createdAt.CompareTo(_sourceItems[mid].MailCopy?.CreationDate ?? DateTime.MinValue); - - if (SortDirection == EmailSortDirection.Descending) - comparison = -comparison; - - if (comparison < 0) - right = mid; - else - left = mid + 1; - } - - return left; - } - - private GroupHeaderBase GetOrCreateGroupHeader(string groupKey) - { - if (!_groupHeaders.TryGetValue(groupKey, out var groupHeader)) - { - groupHeader = CreateGroupHeader(groupKey); - _groupHeaders[groupKey] = groupHeader; - _groupItems[groupKey] = []; - } - return groupHeader; - } - - private GroupHeaderBase CreateGroupHeader(string groupKey) - { - return GroupingType switch - { - EmailGroupingType.ByFromName => new SenderGroupHeader(groupKey), - EmailGroupingType.ByDate when DateTime.TryParse(groupKey, out var date) => new DateGroupHeader(date), - EmailGroupingType.ByDate => new DateGroupHeader(DateTime.Today), - _ => new SenderGroupHeader(groupKey) - }; - } - - private IComparer GetGroupComparer() - { - return GroupingType switch - { - EmailGroupingType.ByFromName => SortDirection == EmailSortDirection.Descending - ? StringComparer.OrdinalIgnoreCase.Reverse() - : StringComparer.OrdinalIgnoreCase, - EmailGroupingType.ByDate => SortDirection == EmailSortDirection.Descending - ? CreateDateComparer(descending: true) - : CreateDateComparer(descending: false), - _ => StringComparer.Ordinal - }; - } - - private static IComparer CreateDateComparer(bool descending) - { - return Comparer.Create((x, y) => - { - var dateX = DateTime.TryParse(x, out var dx) ? dx : DateTime.MinValue; - var dateY = DateTime.TryParse(y, out var dy) ? dy : DateTime.MinValue; - - var result = dateX.CompareTo(dateY); - return descending ? -result : result; - }); - } - - private int FindGroupInsertionPosition(string groupKey) - { - var comparer = GetGroupComparer(); - - if (_groupHeaderIndexCache.Count == 0) - return 0; - - var sortedGroups = _groupHeaderIndexCache.Keys.OrderBy(k => k, comparer).ToList(); - var insertPosition = 0; - - for (int i = 0; i < sortedGroups.Count; i++) - { - var existingGroupKey = sortedGroups[i]; - var comparison = comparer.Compare(groupKey, existingGroupKey); - - if (comparison < 0) - { - insertPosition = _groupHeaderIndexCache[existingGroupKey]; - break; - } - else if (i == sortedGroups.Count - 1) - { - var lastGroupHeaderIndex = _groupHeaderIndexCache[existingGroupKey]; - var lastGroupItemCount = _groupItems[existingGroupKey].Count; - insertPosition = lastGroupHeaderIndex + 1 + lastGroupItemCount; - } - } - - return insertPosition; - } - - private int FindGroupEndIndex(int headerIndex) - { - var groupKey = string.Empty; - foreach (var kvp in _groupHeaderIndexCache) - { - if (kvp.Value == headerIndex) - { - groupKey = kvp.Key; - break; - } - } - - return headerIndex + 1 + _groupItems.GetValueOrDefault(groupKey, []).Count; - } - - private int FindItemInsertionIndexInGroup(object item, int groupStartIndex, int groupEndIndex) - { - var itemDate = GetEffectiveDate(item); - - for (int i = groupStartIndex + 1; i < groupEndIndex; i++) - { - var existingItem = Items[i]; - var existingDate = GetEffectiveDate(existingItem); - - var comparison = itemDate.CompareTo(existingDate); - if (SortDirection == EmailSortDirection.Descending) - comparison = -comparison; - - if (comparison < 0) - return i; - } - - return groupEndIndex; - } - - private void UpdateHeaderIndicesAfterInsertion(int insertIndex, int itemCount = 1) - { - var keysToUpdate = _groupHeaderIndexCache - .Where(kvp => kvp.Value > insertIndex) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in keysToUpdate) - { - _groupHeaderIndexCache[key] += itemCount; - } - } - - private void UpdateHeaderIndicesAfterRemoval(int removeIndex) - { - var keysToUpdate = _groupHeaderIndexCache - .Where(kvp => kvp.Value > removeIndex) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in keysToUpdate) - { - _groupHeaderIndexCache[key]--; - } - } - - private void UpdateAllGroupHeaderCounts() - { - foreach (var (groupKey, groupHeader) in _groupHeaders) - { - UpdateGroupHeaderCounts(groupKey, groupHeader); - } - } - - private void UpdateGroupHeaderCounts(string groupKey, GroupHeaderBase groupHeader) - { - var emailsInGroup = _sourceItems.Where(e => GetGroupKey(e) == groupKey).ToList(); - var expandersInGroup = _threadExpanders.Values - .Where(exp => GetGroupKeyForItem(exp) == groupKey) - .ToList(); - - var totalEmailCount = emailsInGroup.Count; - var unreadCount = emailsInGroup.Count(e => e.MailCopy?.IsRead == false); - - groupHeader.ItemCount = totalEmailCount; - groupHeader.UnreadCount = unreadCount; - } - - private void UpdateGroupAfterChanges() - { - // Update all group header counts and remove empty groups - var groupsToRemove = new List(); - - foreach (var (groupKey, groupHeader) in _groupHeaders.ToList()) - { - UpdateGroupHeaderCounts(groupKey, groupHeader); - - if (groupHeader.ItemCount == 0) - { - groupsToRemove.Add(groupKey); - } - } - - foreach (var groupKey in groupsToRemove) - { - RemoveGroupHeader(groupKey); - } - } - - private void RemoveGroupHeader(string groupKey) - { - if (_groupHeaderIndexCache.TryGetValue(groupKey, out var headerIndex)) - { - Items.RemoveAt(headerIndex); - UpdateHeaderIndicesAfterRemoval(headerIndex); - - _groupHeaderIndexCache.Remove(groupKey); - _groupHeaders.Remove(groupKey); - _groupItems.Remove(groupKey); - } - } - - #region Incremental Refresh Helper Methods - - /// - /// Updates thread expanders when new emails are added - /// - private void UpdateThreadExpandersForNewEmails(IList newEmails) - { - // Group new emails by ThreadId - var newThreadGroups = newEmails - .Where(e => !string.IsNullOrEmpty(e.MailCopy?.ThreadId)) - .GroupBy(e => e.MailCopy!.ThreadId!) - .ToList(); - - foreach (var threadGroup in newThreadGroups) - { - var threadId = threadGroup.Key; - - if (_threadExpanders.TryGetValue(threadId, out var existingExpander)) - { - // Add new emails to existing thread - foreach (var email in threadGroup) - { - existingExpander.AddEmail(email); - email.IsDisplayedInThread = true; - } - } - else - { - // Check if we need to create a new thread with existing emails - var existingEmailsInThread = _sourceItems - .Where(e => e.MailCopy?.ThreadId == threadId && !threadGroup.Contains(e)) - .ToList(); - - var allThreadEmails = existingEmailsInThread.Concat(threadGroup).ToList(); - - if (allThreadEmails.Count >= 2) - { - // Create new thread expander - var expander = new ThreadMailItemViewModel(threadId); - _threadExpanders[threadId] = expander; - - // Add all emails to the thread - foreach (var email in allThreadEmails) - { - expander.AddEmail(email); - email.IsDisplayedInThread = true; - } - } - } - } - } - - /// - /// Finds the correct position to insert an email in the UI Items collection - /// - private int FindUIInsertionPosition(MailItemViewModel email, string groupKey) - { - // If group doesn't exist yet, find position for new group - if (!_groupHeaderIndexCache.ContainsKey(groupKey)) - { - return FindGroupInsertionPosition(groupKey); - } - - var headerIndex = _groupHeaderIndexCache[groupKey]; - var groupEndIndex = FindGroupEndIndex(headerIndex); - - return FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex); - } - - /// - /// Finds the correct position to insert an email within a group's items list - /// - private int FindGroupInsertionIndex(MailItemViewModel email, List 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; - } - - /// - /// Finds the correct position to insert a group header - /// - private int FindHeaderInsertionPosition(string groupKey, GroupHeaderBase groupHeader) - { - if (_groupHeaderIndexCache.Count == 0) - return 0; - - var comparer = GetGroupComparer(); - var insertPosition = 0; - - foreach (var kvp in _groupHeaderIndexCache.OrderBy(k => k.Key, comparer)) - { - var existingGroupKey = kvp.Key; - var comparison = comparer.Compare(groupKey, existingGroupKey); - - if (comparison < 0) - { - insertPosition = kvp.Value; - break; - } - else - { - var groupEndIndex = FindGroupEndIndex(kvp.Value); - insertPosition = groupEndIndex; - } - } - - return insertPosition; - } - - /// - /// Updates header indices after a new group is inserted - /// - private void UpdateSubsequentHeaderIndices(string insertedGroupKey, int itemCount) - { - var insertedHeaderIndex = _groupHeaderIndexCache[insertedGroupKey]; - - var keysToUpdate = _groupHeaderIndexCache - .Where(kvp => kvp.Key != insertedGroupKey && kvp.Value > insertedHeaderIndex) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in keysToUpdate) - { - _groupHeaderIndexCache[key] += itemCount; - } - } - - /// - /// Updates group header counts for affected groups when new emails are added - /// - private void UpdateGroupHeaderCountsForNewEmails(IList newEmails) - { - var affectedGroups = newEmails - .Select(email => GetGroupKey(email)) - .Distinct() - .ToList(); - - foreach (var groupKey in affectedGroups) - { - if (_groupHeaders.TryGetValue(groupKey, out var groupHeader)) - { - UpdateGroupHeaderCounts(groupKey, groupHeader); - } - } - } - - #endregion - - private void OnSourceItemsChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (!_isUpdating) - { - RefreshGrouping(); - } - } - - #endregion - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - return; - - if (disposing) - { - _sourceItems.CollectionChanged -= OnSourceItemsChanged; - - // Unregister all mail items from selection tracking - foreach (var item in Items) - { - if (item is MailItemViewModel mailItem) - { - UnregisterMailItemFromSelectionTracking(mailItem); - } - } - - // Unregister from messenger - WeakReferenceMessenger.Default.Unregister>(this); - - // Reset IsDisplayedInThread for all emails before disposal - foreach (var email in _sourceItems) - { - email.IsDisplayedInThread = false; - } - - // Dispose all thread expanders - foreach (var expander in _threadExpanders.Values) - { - expander.Dispose(); - } - - _sourceItems.Clear(); - Items.Clear(); - _groupHeaders.Clear(); - _groupHeaderIndexCache.Clear(); - _groupItems.Clear(); - _threadExpanders.Clear(); - _selectedVisibleItems.Clear(); - SelectedVisibleCount = 0; - } - - _disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } -} - -/// -/// Extension method to reverse IComparer for descending sorts -/// -internal static class ComparerExtensions -{ - public static IComparer Reverse(this IComparer comparer) - { - return Comparer.Create((x, y) => comparer.Compare(y, x)); - } -} diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index 9c537862..02f4f93a 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -345,7 +345,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient { existingItem.MailCopy = updatedItem; }); + await ExecuteUIThread(() => { existingItem.NotifyPropertyChanges(); }); } /// @@ -548,7 +548,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient { - CurrentMailDraftItem.MailCopy = updatedMail; - + CurrentMailDraftItem.NotifyPropertyChanges(); DiscardCommand.NotifyCanExecuteChanged(); SendCommand.NotifyCanExecuteChanged(); }); diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index 4bb62d0d..874e090a 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; namespace Wino.Mail.ViewModels.Data; @@ -10,19 +11,23 @@ namespace Wino.Mail.ViewModels.Data; /// public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, IMailListItem { - public DateTime CreationDate => MailCopy.CreationDate; - [ObservableProperty] - public partial MailCopy MailCopy { get; set; } = mailCopy; + public MailCopy MailCopy { get; } = mailCopy; [ObservableProperty] - public partial bool ThumbnailUpdatedEvent { get; set; } = false; + public partial bool IsDisplayedInThread { get; set; } [ObservableProperty] [NotifyPropertyChangedRecipients] public partial bool IsSelected { get; set; } + public DateTime CreationDate + { + get => MailCopy.CreationDate; + set => SetProperty(MailCopy.CreationDate, value, MailCopy, (u, n) => u.CreationDate = n); + } + [ObservableProperty] - public partial bool IsDisplayedInThread { get; set; } + public partial bool ThumbnailUpdatedEvent { get; set; } = false; public bool IsFlagged { @@ -90,6 +95,86 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n); } + public MailImportance Importance + { + get => MailCopy.Importance; + set => SetProperty(MailCopy.Importance, value, MailCopy, (u, n) => u.Importance = n); + } + + public string ThreadId + { + get => MailCopy.ThreadId; + set => SetProperty(MailCopy.ThreadId, value, MailCopy, (u, n) => u.ThreadId = n); + } + + public string MessageId + { + get => MailCopy.MessageId; + set => SetProperty(MailCopy.MessageId, value, MailCopy, (u, n) => u.MessageId = n); + } + + public string References + { + get => MailCopy.References; + set => SetProperty(MailCopy.References, value, MailCopy, (u, n) => u.References = n); + } + + public string InReplyTo + { + get => MailCopy.InReplyTo; + set => SetProperty(MailCopy.InReplyTo, value, MailCopy, (u, n) => u.InReplyTo = n); + } + + public Guid FileId + { + get => MailCopy.FileId; + set => SetProperty(MailCopy.FileId, value, MailCopy, (u, n) => u.FileId = n); + } + + public Guid FolderId + { + get => MailCopy.FolderId; + set => SetProperty(MailCopy.FolderId, value, MailCopy, (u, n) => u.FolderId = n); + } + + public Guid UniqueId + { + get => MailCopy.UniqueId; + set => SetProperty(MailCopy.UniqueId, value, MailCopy, (u, n) => u.UniqueId = n); + } + + public string Base64ContactPicture + { + get => MailCopy.SenderContact?.Base64ContactPicture ?? string.Empty; + set => SetProperty(MailCopy.SenderContact.Base64ContactPicture, value, MailCopy, (u, n) => u.SenderContact.Base64ContactPicture = n); + } + + public void NotifyPropertyChanges() + { + // Raise on property changes for all observable properties. + OnPropertyChanged(nameof(CreationDate)); + OnPropertyChanged(nameof(IsFlagged)); + OnPropertyChanged(nameof(FromName)); + OnPropertyChanged(nameof(IsFocused)); + OnPropertyChanged(nameof(IsRead)); + OnPropertyChanged(nameof(IsDraft)); + OnPropertyChanged(nameof(DraftId)); + OnPropertyChanged(nameof(Id)); + OnPropertyChanged(nameof(Subject)); + OnPropertyChanged(nameof(PreviewText)); + OnPropertyChanged(nameof(FromAddress)); + OnPropertyChanged(nameof(HasAttachments)); + OnPropertyChanged(nameof(Importance)); + OnPropertyChanged(nameof(ThreadId)); + OnPropertyChanged(nameof(MessageId)); + OnPropertyChanged(nameof(References)); + OnPropertyChanged(nameof(InReplyTo)); + OnPropertyChanged(nameof(FileId)); + OnPropertyChanged(nameof(FolderId)); + OnPropertyChanged(nameof(UniqueId)); + OnPropertyChanged(nameof(Base64ContactPicture)); + } + public IEnumerable GetContainingIds() => [MailCopy.UniqueId]; public IEnumerable GetSelectedMailItems() diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index 9cced05f..736f4771 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -3,18 +3,17 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Enums; namespace Wino.Mail.ViewModels.Data; /// /// Thread mail item (multiple IMailItem) view model representation. /// -public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable, IMailListItem +public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListItem { private readonly string _threadId; - private bool _disposed; - [ObservableProperty] [NotifyPropertyChangedRecipients] [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] @@ -35,23 +34,106 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable, /// /// Gets the latest email's subject for display /// - public string Subject => ThreadEmails - .OrderByDescending(e => e.MailCopy?.CreationDate) - .FirstOrDefault()?.MailCopy?.Subject; + public string Subject => latestMailViewModel?.MailCopy?.Subject; /// /// Gets the latest email's sender name for display /// - public string FromName => ThreadEmails - .OrderByDescending(e => e.MailCopy?.CreationDate) - .FirstOrDefault()?.MailCopy?.SenderContact.Name; + public string FromName => latestMailViewModel?.MailCopy?.SenderContact.Name; /// /// Gets the latest email's creation date for sorting /// - public DateTime CreationDate => ThreadEmails - .OrderByDescending(e => e.MailCopy?.CreationDate) - .FirstOrDefault()?.MailCopy?.CreationDate ?? DateTime.MinValue; + public DateTime CreationDate => latestMailViewModel?.MailCopy?.CreationDate ?? DateTime.MinValue; + + /// + /// Gets the latest email's sender address for display + /// + public string FromAddress => latestMailViewModel?.FromAddress ?? string.Empty; + + /// + /// Gets the preview text from the latest email + /// + public string PreviewText => latestMailViewModel?.PreviewText ?? string.Empty; + + /// + /// Gets whether any email in this thread has attachments + /// + public bool HasAttachments => ThreadEmails.Any(e => e.HasAttachments); + + /// + /// Gets whether any email in this thread is flagged + /// + public bool IsFlagged => ThreadEmails.Any(e => e.IsFlagged); + + /// + /// Gets whether the latest email is focused + /// + public bool IsFocused => latestMailViewModel?.IsFocused ?? false; + + /// + /// Gets whether all emails in this thread are read + /// + public bool IsRead => ThreadEmails.All(e => e.IsRead); + + /// + /// Gets whether any email in this thread is a draft + /// + public bool IsDraft => ThreadEmails.Any(e => e.IsDraft); + + /// + /// Gets the draft ID from the latest email if it's a draft + /// + public string DraftId => latestMailViewModel?.DraftId ?? string.Empty; + + /// + /// Gets the ID from the latest email + /// + public string Id => latestMailViewModel?.Id ?? string.Empty; + + /// + /// Gets the importance of the latest email + /// + public MailImportance Importance => latestMailViewModel?.Importance ?? MailImportance.Normal; + + /// + /// Gets the thread ID from the latest email + /// + public string ThreadId => latestMailViewModel?.ThreadId ?? _threadId; + + /// + /// Gets the message ID from the latest email + /// + public string MessageId => latestMailViewModel?.MessageId ?? string.Empty; + + /// + /// Gets the references from the latest email + /// + public string References => latestMailViewModel?.References ?? string.Empty; + + /// + /// Gets the in-reply-to from the latest email + /// + public string InReplyTo => latestMailViewModel?.InReplyTo ?? string.Empty; + + /// + /// Gets the file ID from the latest email + /// + public Guid FileId => latestMailViewModel?.FileId ?? Guid.Empty; + + /// + /// Gets the folder ID from the latest email + /// + public Guid FolderId => latestMailViewModel?.FolderId ?? Guid.Empty; + + /// + /// Gets the unique ID from the latest email + /// + public Guid UniqueId => latestMailViewModel?.UniqueId ?? Guid.Empty; + + public string Base64ContactPicture => latestMailViewModel?.MailCopy?.SenderContact?.Base64ContactPicture ?? string.Empty; + + public bool ThumbnailUpdatedEvent => latestMailViewModel?.ThumbnailUpdatedEvent ?? false; /// /// Gets all emails in this thread (observable) @@ -60,42 +142,40 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable, [ObservableProperty] public partial ObservableCollection ThreadEmails { get; set; } = []; - public MailItemViewModel LatestMailViewModel => ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).FirstOrDefault()!; + private MailItemViewModel latestMailViewModel => ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).FirstOrDefault()!; public ThreadMailItemViewModel(string threadId) { _threadId = threadId; } - protected virtual void Dispose(bool disposing) - { - if (_disposed) - return; - - if (disposing) - { - ThreadEmails.Clear(); - } - - _disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - public void NotifyPropertyChanges() { OnPropertyChanged(nameof(Subject)); OnPropertyChanged(nameof(FromName)); OnPropertyChanged(nameof(CreationDate)); - OnPropertyChanged(nameof(LatestMailViewModel)); + OnPropertyChanged(nameof(FromAddress)); + OnPropertyChanged(nameof(PreviewText)); + OnPropertyChanged(nameof(HasAttachments)); + OnPropertyChanged(nameof(IsFlagged)); + OnPropertyChanged(nameof(IsFocused)); + OnPropertyChanged(nameof(IsRead)); + OnPropertyChanged(nameof(IsDraft)); + OnPropertyChanged(nameof(DraftId)); + OnPropertyChanged(nameof(Id)); + OnPropertyChanged(nameof(Importance)); + OnPropertyChanged(nameof(ThreadId)); + OnPropertyChanged(nameof(MessageId)); + OnPropertyChanged(nameof(References)); + OnPropertyChanged(nameof(InReplyTo)); + OnPropertyChanged(nameof(FileId)); + OnPropertyChanged(nameof(FolderId)); + OnPropertyChanged(nameof(UniqueId)); OnPropertyChanged(nameof(ThreadEmails)); + OnPropertyChanged(nameof(EmailCount)); + OnPropertyChanged(nameof(Base64ContactPicture)); } - /// /// Adds an email to this thread /// diff --git a/Wino.Mail.WinUI/AppShell.xaml.cs b/Wino.Mail.WinUI/AppShell.xaml.cs index f395d1c0..898aff16 100644 --- a/Wino.Mail.WinUI/AppShell.xaml.cs +++ b/Wino.Mail.WinUI/AppShell.xaml.cs @@ -340,7 +340,7 @@ public sealed partial class AppShell : AppShellAbstract, protected override void RegisterRecipients() { base.RegisterRecipients(); - + WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -350,7 +350,7 @@ public sealed partial class AppShell : AppShellAbstract, protected override void UnregisterRecipients() { base.UnregisterRecipients(); - + WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); diff --git a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml index c3be3fa9..1d51c1a4 100644 --- a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml +++ b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml @@ -49,7 +49,7 @@ VerticalAlignment="Top" Canvas.ZIndex="0" Fill="{ThemeResource SystemAccentColor}" - Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(MailItem.IsRead), Mode=OneWay}" /> + Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(IsRead), Mode=OneWay}" /> @@ -73,9 +73,9 @@ HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="14" - FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}" - FromName="{x:Bind MailItem.FromName, Mode=OneWay}" - SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}" + FromAddress="{x:Bind FromAddress, Mode=OneWay}" + FromName="{x:Bind FromName, Mode=OneWay}" + SenderContactPicture="{x:Bind Base64ContactPicture, Mode=OneWay}" ThumbnailUpdatedEvent="{x:Bind IsThumbnailUpdated, Mode=OneWay}" Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" /> @@ -104,7 +104,7 @@ @@ -114,17 +114,17 @@ + Visibility="{x:Bind helpers:XamlHelpers.StringToVisibilityConverter(FromName)}" /> + Visibility="{x:Bind helpers:XamlHelpers.StringToVisibilityReversedConverter(FromName)}" /> + Text="{x:Bind helpers:XamlHelpers.GetMailItemDisplaySummaryForListing(IsDraft, CreationDate, Prefer24HourTimeFormat)}" /> @@ -219,10 +220,10 @@ @@ -236,12 +237,12 @@ @@ -255,7 +256,7 @@ - + diff --git a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs index c9ce1fdc..00051193 100644 --- a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs @@ -1,13 +1,14 @@ -using System.Numerics; +using System; +using System.Linq; +using System.Numerics; using System.Windows.Input; using CommunityToolkit.WinUI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Wino.Core.Domain; -using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.MailItem; using Wino.Extensions; +using Wino.Mail.ViewModels.Data; namespace Wino.Controls; @@ -17,116 +18,88 @@ public sealed partial class MailItemDisplayInformationControl : UserControl public bool IsRunningHoverAction { get; set; } - public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(MailListDisplayMode), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailListDisplayMode.Spacious)); - public static readonly DependencyProperty ShowPreviewTextProperty = DependencyProperty.Register(nameof(ShowPreviewText), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); - public static readonly DependencyProperty IsAvatarVisibleProperty = DependencyProperty.Register(nameof(IsAvatarVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); - public static readonly DependencyProperty IsSubjectVisibleProperty = DependencyProperty.Register(nameof(IsSubjectVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); - public static readonly DependencyProperty ConnectedExpanderProperty = DependencyProperty.Register(nameof(ConnectedExpander), typeof(WinoExpander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); - public static readonly DependencyProperty LeftHoverActionProperty = DependencyProperty.Register(nameof(LeftHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); - public static readonly DependencyProperty CenterHoverActionProperty = DependencyProperty.Register(nameof(CenterHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); - public static readonly DependencyProperty RightHoverActionProperty = DependencyProperty.Register(nameof(RightHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); - public static readonly DependencyProperty HoverActionExecutedCommandProperty = DependencyProperty.Register(nameof(HoverActionExecutedCommand), typeof(ICommand), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); - public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(MailCopy), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null, new PropertyChangedCallback(OnMailItemChanged))); - public static readonly DependencyProperty IsHoverActionsEnabledProperty = DependencyProperty.Register(nameof(IsHoverActionsEnabled), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); - public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); - public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); - public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); - public static readonly DependencyProperty IsThumbnailUpdatedProperty = DependencyProperty.Register(nameof(IsThumbnailUpdated), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); + [GeneratedDependencyProperty(DefaultValue = MailListDisplayMode.Spacious)] + public partial MailListDisplayMode DisplayMode { get; set; } - public bool IsThumbnailUpdated - { - get { return (bool)GetValue(IsThumbnailUpdatedProperty); } - set { SetValue(IsThumbnailUpdatedProperty, value); } - } + [GeneratedDependencyProperty(DefaultValue = true)] + public partial bool ShowPreviewText { get; set; } - public bool IsThreadExpanded - { - get { return (bool)GetValue(IsThreadExpandedProperty); } - set { SetValue(IsThreadExpandedProperty, value); } - } + [GeneratedDependencyProperty(DefaultValue = true)] + public partial bool IsAvatarVisible { get; set; } - public bool IsThreadExpanderVisible - { - get { return (bool)GetValue(IsThreadExpanderVisibleProperty); } - set { SetValue(IsThreadExpanderVisibleProperty, value); } - } + [GeneratedDependencyProperty(DefaultValue = true)] + public partial bool IsSubjectVisible { get; set; } - public bool Prefer24HourTimeFormat - { - get { return (bool)GetValue(Prefer24HourTimeFormatProperty); } - set { SetValue(Prefer24HourTimeFormatProperty, value); } - } + #region Display Properties - public bool IsHoverActionsEnabled - { - get { return (bool)GetValue(IsHoverActionsEnabledProperty); } - set { SetValue(IsHoverActionsEnabledProperty, value); } - } + [GeneratedDependencyProperty] + public partial string? Subject { get; set; } - public MailCopy MailItem - { - get { return (MailCopy)GetValue(MailItemProperty); } - set { SetValue(MailItemProperty, value); } - } + [GeneratedDependencyProperty] + public partial string? FromName { get; set; } - public ICommand HoverActionExecutedCommand - { - get { return (ICommand)GetValue(HoverActionExecutedCommandProperty); } - set { SetValue(HoverActionExecutedCommandProperty, value); } - } + [GeneratedDependencyProperty] + public partial string? FromAddress { get; set; } - public MailOperation LeftHoverAction - { - get { return (MailOperation)GetValue(LeftHoverActionProperty); } - set { SetValue(LeftHoverActionProperty, value); } - } + [GeneratedDependencyProperty] + public partial string? PreviewText { get; set; } - public MailOperation CenterHoverAction - { - get { return (MailOperation)GetValue(CenterHoverActionProperty); } - set { SetValue(CenterHoverActionProperty, value); } - } + [GeneratedDependencyProperty] + public partial bool IsRead { get; set; } - public MailOperation RightHoverAction - { - get { return (MailOperation)GetValue(RightHoverActionProperty); } - set { SetValue(RightHoverActionProperty, value); } - } + [GeneratedDependencyProperty] + public partial bool IsDraft { get; set; } - public WinoExpander ConnectedExpander - { - get { return (WinoExpander)GetValue(ConnectedExpanderProperty); } - set { SetValue(ConnectedExpanderProperty, value); } - } + [GeneratedDependencyProperty] + public partial bool HasAttachments { get; set; } - public bool IsSubjectVisible - { - get { return (bool)GetValue(IsSubjectVisibleProperty); } - set { SetValue(IsSubjectVisibleProperty, value); } - } + [GeneratedDependencyProperty] + public partial bool IsFlagged { get; set; } - public bool IsAvatarVisible - { - get { return (bool)GetValue(IsAvatarVisibleProperty); } - set { SetValue(IsAvatarVisibleProperty, value); } - } + [GeneratedDependencyProperty] + public partial DateTime CreationDate { get; set; } + [GeneratedDependencyProperty] + public partial string? Base64ContactPicture { get; set; } - public bool ShowPreviewText - { - get { return (bool)GetValue(ShowPreviewTextProperty); } - set { SetValue(ShowPreviewTextProperty, value); } - } + #endregion - public MailListDisplayMode DisplayMode - { - get { return (MailListDisplayMode)GetValue(DisplayModeProperty); } - set { SetValue(DisplayModeProperty, value); } - } + [GeneratedDependencyProperty] + public partial WinoExpander? ConnectedExpander { get; set; } + + [GeneratedDependencyProperty(DefaultValue = MailOperation.None)] + public partial MailOperation LeftHoverAction { get; set; } + + [GeneratedDependencyProperty(DefaultValue = MailOperation.None)] + public partial MailOperation CenterHoverAction { get; set; } + + [GeneratedDependencyProperty(DefaultValue = MailOperation.None)] + public partial MailOperation RightHoverAction { get; set; } + + [GeneratedDependencyProperty] + public partial ICommand? HoverActionExecutedCommand { get; set; } + + [GeneratedDependencyProperty(DefaultValue = true)] + public partial bool IsHoverActionsEnabled { get; set; } + + [GeneratedDependencyProperty(DefaultValue = false)] + public partial bool Prefer24HourTimeFormat { get; set; } + + [GeneratedDependencyProperty(DefaultValue = false)] + public partial bool IsThreadExpanderVisible { get; set; } + + [GeneratedDependencyProperty(DefaultValue = false)] + public partial bool IsThreadExpanded { get; set; } + + [GeneratedDependencyProperty(DefaultValue = false)] + public partial bool IsThumbnailUpdated { get; set; } + + [GeneratedDependencyProperty] + public partial IMailListItem? ActionItem { get; set; } public MailItemDisplayInformationControl() { - this.InitializeComponent(); + InitializeComponent(); var compositor = this.Visual().Compositor; @@ -142,19 +115,9 @@ public sealed partial class MailItemDisplayInformationControl : UserControl RootContainerVisualWrapper.SizeChanged += (s, e) => leftBackgroundVisual.Size = e.NewSize.ToVector2(); } - private static void OnMailItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) + partial void OnIsFlaggedChanged(bool newValue) { - if (obj is MailItemDisplayInformationControl control) - { - control.UpdateInformation(); - } - } - private void UpdateInformation() - { - if (MailItem == null) return; - - TitleText.Text = string.IsNullOrWhiteSpace(MailItem.Subject) ? Translator.MailItemNoSubject : MailItem.Subject; } private void ControlPointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) @@ -179,14 +142,13 @@ public sealed partial class MailItemDisplayInformationControl : UserControl { IsRunningHoverAction = true; - MailOperationPreperationRequest package = null; + MailOperationPreperationRequest? package = null; - //if (MailItem is MailCopy mailCopy) - // package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true); - //else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel) - // package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true); - //else if (MailItem is ThreadMailItem threadMailItem) - // package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast().Select(a => a.MailCopy), toggleExecution: true); + if (ActionItem is MailItemViewModel mailItemViewModel) + package = new MailOperationPreperationRequest(operation, mailItemViewModel.MailCopy, toggleExecution: true); + + else if (ActionItem is ThreadMailItemViewModel threadMailItemViewModel) + package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy), toggleExecution: true); if (package == null) return; diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml b/Wino.Mail.WinUI/Views/MailListPage.xaml index 2bdf8754..98b48569 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml @@ -54,18 +54,28 @@ + ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" + Subject="{x:Bind Subject, Mode=OneWay}" /> @@ -76,20 +86,30 @@ + ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" + Subject="{x:Bind Subject, Mode=OneWay}" /> + FromAddress="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.FromAddress}" + FromName="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.FromName}" + ShowPreviewText="False" + Subject="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.Subject}" /> + FromAddress="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.FromAddress}" + FromName="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.FromName}" + ShowPreviewText="True" + Subject="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.Subject}" /> + FromAddress="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.FromAddress}" + FromName="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.FromName}" + ShowPreviewText="True" + Subject="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy.Subject}" />