diff --git a/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs b/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs index 74ffa5eb..9cf9a777 100644 --- a/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs +++ b/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs @@ -13,22 +13,26 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List Mail { public override void ApplyUIChanges() { + if (MailsToMarkRead == null || MailsToMarkRead.Count == 0) return; + foreach (var item in MailsToMarkRead) { item.IsRead = true; - - WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item)); } + + WeakReferenceMessenger.Default.Send(new BulkMailUpdatedMessage(MailsToMarkRead)); } public override void RevertUIChanges() { + if (MailsToMarkRead == null || MailsToMarkRead.Count == 0) return; + foreach (var item in MailsToMarkRead) { item.IsRead = false; - - WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item)); } + + WeakReferenceMessenger.Default.Send(new BulkMailUpdatedMessage(MailsToMarkRead)); } public List SynchronizationFolderIds => [Folder.Id]; diff --git a/Wino.Mail.ViewModels/MailBaseViewModel.cs b/Wino.Mail.ViewModels/MailBaseViewModel.cs index 33493cb8..084c498d 100644 --- a/Wino.Mail.ViewModels/MailBaseViewModel.cs +++ b/Wino.Mail.ViewModels/MailBaseViewModel.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.Messaging; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Models.Folders; @@ -11,6 +12,7 @@ public class MailBaseViewModel : CoreBaseViewModel, IRecipient, IRecipient, IRecipient, + IRecipient, IRecipient, IRecipient, IRecipient, @@ -21,6 +23,17 @@ public class MailBaseViewModel : CoreBaseViewModel, protected virtual void OnMailAdded(MailCopy addedMail) { } protected virtual void OnMailRemoved(MailCopy removedMail) { } protected virtual void OnMailUpdated(MailCopy updatedMail) { } + + protected virtual void OnMailUpdated(IReadOnlyList updatedMails) + { + if (updatedMails == null) return; + + foreach (var mail in updatedMails) + { + OnMailUpdated(mail); + } + } + protected virtual void OnMailDownloaded(MailCopy downloadedMail) { } protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { } protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { } @@ -31,6 +44,7 @@ public class MailBaseViewModel : CoreBaseViewModel, void IRecipient.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail); void IRecipient.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail); void IRecipient.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail); + void IRecipient.Receive(BulkMailUpdatedMessage message) => OnMailUpdated(message.UpdatedMails); void IRecipient.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail); void IRecipient.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId); diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index b4fd27ad..d7bfc2a0 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -685,6 +685,37 @@ public partial class MailListPageViewModel : MailBaseViewModel, await ExecuteUIThread(() => { SetupTopBarActions(); }); } + protected override async void OnMailUpdated(IReadOnlyList updatedMails) + { + base.OnMailUpdated(updatedMails); + + if (updatedMails == null || updatedMails.Count == 0) return; + + // Bulk update: do all changes in a single UI-thread invocation to avoid UI lockups when + // thousands of MailUpdatedMessage events would otherwise be processed one by one. + await ExecuteUIThread(() => + { + foreach (var mail in updatedMails) + { + if (mail == null) continue; + + // Avoid work for items not in the list. + if (!MailCollection.MailCopyIdHashSet.Contains(mail.UniqueId)) + continue; + + var container = MailCollection.GetMailItemContainer(mail.UniqueId); + + if (container?.ItemViewModel != null) + { + container.ItemViewModel.MailCopy = mail; + container.ThreadViewModel?.NotifyPropertyChanges(); + } + } + + SetupTopBarActions(); + }); + } + protected override async void OnMailRemoved(MailCopy removedMail) { base.OnMailRemoved(removedMail); diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 3a2818c6..5fccd83a 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -596,13 +596,22 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // This is done with UniqueId to include FolderId into calculations. if (initializedMailItemViewModel.UniqueId != updatedMail.UniqueId) return; - // Mail operation might change the mail item like mark read/unread or change flag. - // So we need to update the mail item view model when this happens. - // Also command bar items must be re-initialized since the items loaded based on the mail item. - await ExecuteUIThread(() => { InitializeCommandBarItems(); }); } + protected override async void OnMailUpdated(IReadOnlyList updatedMails) + { + base.OnMailUpdated(updatedMails); + + if (initializedMailItemViewModel == null || updatedMails == null || updatedMails.Count == 0) return; + + // Only care about the currently rendered item. + if (updatedMails.Any(m => m?.UniqueId == initializedMailItemViewModel.UniqueId)) + { + await ExecuteUIThread(() => { InitializeCommandBarItems(); }); + } + } + [RelayCommand] private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel) { diff --git a/Wino.Messages/UI/BulkMailUpdatedMessage.cs b/Wino.Messages/UI/BulkMailUpdatedMessage.cs new file mode 100644 index 00000000..f4335f54 --- /dev/null +++ b/Wino.Messages/UI/BulkMailUpdatedMessage.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; + +namespace Wino.Messaging.UI; + +public record BulkMailUpdatedMessage(IReadOnlyList UpdatedMails) : UIMessageBase;