using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Mail.ViewModels.Data;
///
/// Thread mail item (multiple IMailItem) view model representation.
///
public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListItem, IMailItemDisplayInformation
{
private readonly string _threadId;
private readonly HashSet _uniqueIdSet = [];
private MailItemViewModel _cachedLatestMailViewModel;
private int _suspendChildPropertyNotificationsCount;
[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 a calendar invitation.
///
public bool IsCalendarEvent => ThreadEmails.Any(e => e.IsCalendarEvent);
///
/// 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;
public AccountContact SenderContact => latestMailViewModel?.MailCopy?.SenderContact;
///
/// 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(IsCalendarEvent))]
[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))]
[NotifyPropertyChangedFor(nameof(SenderContact))]
public partial ObservableCollection ThreadEmails { get; set; } = [];
private MailItemViewModel latestMailViewModel => _cachedLatestMailViewModel;
public DateTime SortingDate => CreationDate;
public string SortingName => FromName;
public ThreadMailItemViewModel(string threadId)
{
_threadId = threadId;
}
internal void SuspendChildPropertyNotifications() => _suspendChildPropertyNotificationsCount++;
internal void ResumeChildPropertyNotifications()
{
if (_suspendChildPropertyNotificationsCount > 0)
{
_suspendChildPropertyNotificationsCount--;
}
}
private void RefreshLatestMailCache()
{
_cachedLatestMailViewModel = ThreadEmails
.OrderByDescending(static item => item.MailCopy.CreationDate)
.FirstOrDefault();
}
///
/// 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);
email.PropertyChanged += ThreadEmailPropertyChanged;
_uniqueIdSet.Add(email.MailCopy.UniqueId);
RefreshLatestMailCache();
OnPropertyChanged(nameof(EmailCount));
NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
}
///
/// Removes an email from this thread
///
public void RemoveEmail(MailItemViewModel email)
{
if (ThreadEmails.Remove(email))
{
email.PropertyChanged -= ThreadEmailPropertyChanged;
_uniqueIdSet.Remove(email.MailCopy.UniqueId);
RefreshLatestMailCache();
OnPropertyChanged(nameof(EmailCount));
NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
}
}
public void UnregisterThreadEmailPropertyChangedHandlers()
{
foreach (var email in ThreadEmails)
{
email.PropertyChanged -= ThreadEmailPropertyChanged;
}
}
private void ThreadEmailPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_suspendChildPropertyNotificationsCount > 0)
return;
if (sender is not MailItemViewModel updatedMailItem)
return;
if (e.PropertyName == nameof(MailItemViewModel.IsSelected) ||
e.PropertyName == nameof(MailItemViewModel.IsDisplayedInThread) ||
e.PropertyName == nameof(MailItemViewModel.IsBusy))
{
return;
}
if (e.PropertyName == nameof(MailItemViewModel.ThumbnailUpdatedEvent))
{
if (ReferenceEquals(updatedMailItem, latestMailViewModel))
{
OnPropertyChanged(nameof(ThumbnailUpdatedEvent));
}
return;
}
var changedFlags = string.IsNullOrEmpty(e.PropertyName)
? MailCopyChangeFlags.All
: MailItemViewModel.GetChangeFlagsForProperty(e.PropertyName);
if (changedFlags == MailCopyChangeFlags.None)
{
NotifyMailItemUpdated(updatedMailItem, MailCopyChangeFlags.All);
return;
}
NotifyMailItemUpdated(updatedMailItem, changedFlags);
}
///
/// Notifies that a mail item within this thread has been updated.
///
/// The mail item that was updated (can be null to refresh all).
/// Set of changed child fields.
public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem, MailCopyChangeFlags changedFlags = MailCopyChangeFlags.All)
{
if (changedFlags == MailCopyChangeFlags.None)
return;
var previousLatest = latestMailViewModel;
if (changedFlags == MailCopyChangeFlags.All ||
(changedFlags & MailCopyChangeFlags.CreationDate) != 0 ||
previousLatest == null ||
!ThreadEmails.Contains(previousLatest))
{
RefreshLatestMailCache();
}
var currentLatest = latestMailViewModel;
var latestChanged = !ReferenceEquals(previousLatest, currentLatest);
var updatesDisplayedLatest = changedFlags == MailCopyChangeFlags.All ||
updatedMailItem == null ||
latestChanged ||
ReferenceEquals(updatedMailItem, previousLatest) ||
ReferenceEquals(updatedMailItem, currentLatest);
var changedProperties = new List(10);
void Queue(string propertyName)
{
if (!changedProperties.Contains(propertyName))
{
changedProperties.Add(propertyName);
}
}
if (updatesDisplayedLatest)
{
if (changedFlags == MailCopyChangeFlags.All || latestChanged)
{
Queue(nameof(Subject));
Queue(nameof(FromName));
Queue(nameof(CreationDate));
Queue(nameof(FromAddress));
Queue(nameof(PreviewText));
Queue(nameof(IsFocused));
Queue(nameof(DraftId));
Queue(nameof(Id));
Queue(nameof(Importance));
Queue(nameof(ThreadId));
Queue(nameof(MessageId));
Queue(nameof(References));
Queue(nameof(InReplyTo));
Queue(nameof(FileId));
Queue(nameof(FolderId));
Queue(nameof(UniqueId));
Queue(nameof(Base64ContactPicture));
Queue(nameof(SenderContact));
Queue(nameof(ThumbnailUpdatedEvent));
Queue(nameof(SortingDate));
Queue(nameof(SortingName));
}
else
{
if ((changedFlags & MailCopyChangeFlags.Subject) != 0)
Queue(nameof(Subject));
if ((changedFlags & MailCopyChangeFlags.FromName) != 0)
{
Queue(nameof(FromName));
Queue(nameof(SortingName));
}
if ((changedFlags & MailCopyChangeFlags.CreationDate) != 0)
{
Queue(nameof(CreationDate));
Queue(nameof(SortingDate));
}
if ((changedFlags & MailCopyChangeFlags.FromAddress) != 0)
Queue(nameof(FromAddress));
if ((changedFlags & MailCopyChangeFlags.PreviewText) != 0)
Queue(nameof(PreviewText));
if ((changedFlags & MailCopyChangeFlags.IsFocused) != 0)
Queue(nameof(IsFocused));
if ((changedFlags & MailCopyChangeFlags.DraftId) != 0)
Queue(nameof(DraftId));
if ((changedFlags & MailCopyChangeFlags.Id) != 0)
Queue(nameof(Id));
if ((changedFlags & MailCopyChangeFlags.Importance) != 0)
Queue(nameof(Importance));
if ((changedFlags & MailCopyChangeFlags.ThreadId) != 0)
Queue(nameof(ThreadId));
if ((changedFlags & MailCopyChangeFlags.MessageId) != 0)
Queue(nameof(MessageId));
if ((changedFlags & MailCopyChangeFlags.References) != 0)
Queue(nameof(References));
if ((changedFlags & MailCopyChangeFlags.InReplyTo) != 0)
Queue(nameof(InReplyTo));
if ((changedFlags & MailCopyChangeFlags.FileId) != 0)
Queue(nameof(FileId));
if ((changedFlags & MailCopyChangeFlags.FolderId) != 0)
Queue(nameof(FolderId));
if ((changedFlags & MailCopyChangeFlags.UniqueId) != 0)
Queue(nameof(UniqueId));
if ((changedFlags & MailCopyChangeFlags.SenderContact) != 0)
{
Queue(nameof(Base64ContactPicture));
Queue(nameof(SenderContact));
}
}
}
if ((changedFlags & MailCopyChangeFlags.HasAttachments) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(HasAttachments));
if ((changedFlags & MailCopyChangeFlags.ItemType) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsCalendarEvent));
if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsFlagged));
if ((changedFlags & MailCopyChangeFlags.IsRead) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsRead));
if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsDraft));
foreach (var changedProperty in changedProperties)
{
OnPropertyChanged(changedProperty);
}
}
///
/// 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);
}
}
}