using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain; using Wino.Core.Domain.Enums; namespace Wino.Mail.ViewModels.Data; /// /// Thread mail item (multiple IMailItem) view model representation. /// public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListItem { private readonly string _threadId; private readonly HashSet _uniqueIdSet = []; private MailItemViewModel _cachedLatestMailViewModel; [ObservableProperty] [NotifyPropertyChangedRecipients] [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] public partial bool IsThreadExpanded { get; set; } [ObservableProperty] [NotifyPropertyChangedRecipients] [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] public partial bool IsSelected { get; set; } /// /// Direct callback invoked when changes. /// Used by the ListViewItem container to update its IsCustomSelected DP /// without subscribing to INotifyPropertyChanged (faster, AOT-safe). /// public Action OnSelectionChanged { get; set; } partial void OnIsSelectedChanged(bool value) => OnSelectionChanged?.Invoke(value); [ObservableProperty] public partial bool IsBusy { get; set; } public bool IsSelectedOrExpanded => IsSelected || IsThreadExpanded; /// /// Gets the number of emails in this thread /// public int EmailCount => ThreadEmails.Count; /// /// Gets the latest email's subject for display /// public string Subject => latestMailViewModel?.MailCopy?.Subject; /// /// Gets the latest email's sender name for display /// public string FromName => latestMailViewModel?.MailCopy?.FromName ?? Translator.UnknownSender; /// /// Gets the latest email's creation date for sorting /// 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) /// /// [ObservableProperty] [NotifyPropertyChangedFor(nameof(EmailCount))] [NotifyPropertyChangedFor(nameof(Subject))] [NotifyPropertyChangedFor(nameof(FromName))] [NotifyPropertyChangedFor(nameof(CreationDate))] [NotifyPropertyChangedFor(nameof(FromAddress))] [NotifyPropertyChangedFor(nameof(PreviewText))] [NotifyPropertyChangedFor(nameof(HasAttachments))] [NotifyPropertyChangedFor(nameof(IsFlagged))] [NotifyPropertyChangedFor(nameof(IsFocused))] [NotifyPropertyChangedFor(nameof(IsRead))] [NotifyPropertyChangedFor(nameof(IsDraft))] [NotifyPropertyChangedFor(nameof(DraftId))] [NotifyPropertyChangedFor(nameof(Id))] [NotifyPropertyChangedFor(nameof(Importance))] [NotifyPropertyChangedFor(nameof(ThreadId))] [NotifyPropertyChangedFor(nameof(MessageId))] [NotifyPropertyChangedFor(nameof(References))] [NotifyPropertyChangedFor(nameof(InReplyTo))] [NotifyPropertyChangedFor(nameof(FileId))] [NotifyPropertyChangedFor(nameof(FolderId))] [NotifyPropertyChangedFor(nameof(UniqueId))] [NotifyPropertyChangedFor(nameof(Base64ContactPicture))] public partial ObservableCollection ThreadEmails { get; set; } = []; private MailItemViewModel latestMailViewModel => _cachedLatestMailViewModel; public DateTime SortingDate => CreationDate; public string SortingName => FromName; public ThreadMailItemViewModel(string threadId) { _threadId = threadId; } /// /// Adds an email to this thread /// public void AddEmail(MailItemViewModel email) { if (email.MailCopy.ThreadId != _threadId) throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'"); // Insert email in sorted order by CreationDate (newest first, oldest last) var insertIndex = 0; for (int i = 0; i < ThreadEmails.Count; i++) { if (ThreadEmails[i].MailCopy.CreationDate < email.MailCopy.CreationDate) { insertIndex = i; break; } insertIndex = i + 1; } ThreadEmails.Insert(insertIndex, email); _uniqueIdSet.Add(email.MailCopy.UniqueId); _cachedLatestMailViewModel = ThreadEmails[0]; // Reassign to trigger property change notifications ThreadEmails = ThreadEmails; } /// /// Removes an email from this thread /// public void RemoveEmail(MailItemViewModel email) { if (ThreadEmails.Remove(email)) { _uniqueIdSet.Remove(email.MailCopy.UniqueId); _cachedLatestMailViewModel = ThreadEmails.Count > 0 ? ThreadEmails[0] : null; // Reassign to trigger property change notifications ThreadEmails = ThreadEmails; } } /// /// Notifies that a mail item within this thread has been updated. /// This raises PropertyChanged for all thread-level computed properties that depend on child items. /// /// The mail item that was updated (can be null to refresh all). public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem) { // Raise PropertyChanged for all computed properties that depend on ThreadEmails contents OnPropertyChanged(nameof(Subject)); OnPropertyChanged(nameof(FromName)); OnPropertyChanged(nameof(CreationDate)); 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(Base64ContactPicture)); OnPropertyChanged(nameof(ThumbnailUpdatedEvent)); OnPropertyChanged(nameof(SortingDate)); OnPropertyChanged(nameof(SortingName)); } /// /// Checks if this thread contains an email with the specified unique ID /// public bool HasUniqueId(Guid uniqueId) => _uniqueIdSet.Contains(uniqueId); public IEnumerable GetContainingIds() => ThreadEmails.Select(a => a.MailCopy.UniqueId); public IEnumerable GetSelectedMailItems() { if (IsSelected) { // If the thread itself is selected, return all emails in the thread return ThreadEmails; } else { // Otherwise, return only individually selected emails within the thread return ThreadEmails.Where(e => e.IsSelected); } } }