diff --git a/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs
new file mode 100644
index 00000000..f500aa5d
--- /dev/null
+++ b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs
@@ -0,0 +1,57 @@
+using System;
+
+namespace Wino.Core.Domain.Enums;
+
+[Flags]
+public enum MailCopyChangeFlags
+{
+ None = 0,
+ Id = 1 << 0,
+ FolderId = 1 << 1,
+ ThreadId = 1 << 2,
+ MessageId = 1 << 3,
+ References = 1 << 4,
+ InReplyTo = 1 << 5,
+ FromName = 1 << 6,
+ FromAddress = 1 << 7,
+ Subject = 1 << 8,
+ PreviewText = 1 << 9,
+ CreationDate = 1 << 10,
+ Importance = 1 << 11,
+ IsRead = 1 << 12,
+ IsFlagged = 1 << 13,
+ IsFocused = 1 << 14,
+ HasAttachments = 1 << 15,
+ ItemType = 1 << 16,
+ DraftId = 1 << 17,
+ IsDraft = 1 << 18,
+ FileId = 1 << 19,
+ AssignedFolder = 1 << 20,
+ AssignedAccount = 1 << 21,
+ SenderContact = 1 << 22,
+ UniqueId = 1 << 23,
+ All = Id |
+ FolderId |
+ ThreadId |
+ MessageId |
+ References |
+ InReplyTo |
+ FromName |
+ FromAddress |
+ Subject |
+ PreviewText |
+ CreationDate |
+ Importance |
+ IsRead |
+ IsFlagged |
+ IsFocused |
+ HasAttachments |
+ ItemType |
+ DraftId |
+ IsDraft |
+ FileId |
+ AssignedFolder |
+ AssignedAccount |
+ SenderContact |
+ UniqueId
+}
diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
index 93620883..f423eadf 100644
--- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
+++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
@@ -30,6 +31,11 @@ public interface IBaseSynchronizer
/// Mail unique id to check.
bool HasPendingOperation(Guid mailUniqueId);
+ ///
+ /// Returns mail unique ids that currently have queued or executing operations.
+ ///
+ IReadOnlyCollection GetPendingOperationUniqueIds();
+
///
/// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
///
diff --git a/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs b/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs
index 63d133b2..d9b3ded1 100644
--- a/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs
+++ b/Wino.Core/Requests/Folder/MarkFolderAsReadRequest.cs
@@ -20,7 +20,7 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List Mail
item.IsRead = true;
- WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientUpdated));
+ WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead));
}
}
@@ -33,7 +33,7 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List Mail
item.IsRead = false;
- WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientReverted));
+ WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
}
}
diff --git a/Wino.Core/Requests/Mail/ChangeFlagRequest.cs b/Wino.Core/Requests/Mail/ChangeFlagRequest.cs
index 50ab0809..54f33a19 100644
--- a/Wino.Core/Requests/Mail/ChangeFlagRequest.cs
+++ b/Wino.Core/Requests/Mail/ChangeFlagRequest.cs
@@ -31,7 +31,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
Item.IsFlagged = IsFlagged;
- WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated));
+ WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsFlagged));
}
public override void RevertUIChanges()
@@ -41,7 +41,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
Item.IsFlagged = !IsFlagged;
- WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted));
+ WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged));
}
}
diff --git a/Wino.Core/Requests/Mail/MarkReadRequest.cs b/Wino.Core/Requests/Mail/MarkReadRequest.cs
index a7edc8c2..656df721 100644
--- a/Wino.Core/Requests/Mail/MarkReadRequest.cs
+++ b/Wino.Core/Requests/Mail/MarkReadRequest.cs
@@ -30,7 +30,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
Item.IsRead = IsRead;
- WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated));
+ WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead));
}
public override void RevertUIChanges()
@@ -40,7 +40,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
Item.IsRead = !IsRead;
- WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted));
+ WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
}
}
diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs
index 5b1637c5..2cd51dce 100644
--- a/Wino.Core/Synchronizers/BaseSynchronizer.cs
+++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs
@@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -130,6 +131,8 @@ public abstract partial class BaseSynchronizer : ObservableObject,
public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId);
+ public IReadOnlyCollection GetPendingOperationUniqueIds() => _pendingMailOperationIds.Keys.ToArray();
+
public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId);
protected void TrackQueuedRequest(IRequestBase request)
diff --git a/Wino.Mail.ViewModels.Tests/Data/MailItemViewModelUpdateTests.cs b/Wino.Mail.ViewModels.Tests/Data/MailItemViewModelUpdateTests.cs
new file mode 100644
index 00000000..a674ba1f
--- /dev/null
+++ b/Wino.Mail.ViewModels.Tests/Data/MailItemViewModelUpdateTests.cs
@@ -0,0 +1,173 @@
+using FluentAssertions;
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Mail.ViewModels.Collections;
+using Wino.Mail.ViewModels.Data;
+using Xunit;
+
+namespace Wino.Mail.ViewModels.Tests.Data;
+
+public class MailItemViewModelUpdateTests
+{
+ [Fact]
+ public void UpdateFrom_ShouldNotifyOnlyReadState_WhenSameInstanceAndHintProvided()
+ {
+ var mailCopy = CreateMailCopy("thread-1", DateTime.UtcNow);
+ var sut = new MailItemViewModel(mailCopy);
+ var raisedProperties = new List();
+
+ sut.PropertyChanged += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.PropertyName))
+ {
+ raisedProperties.Add(e.PropertyName);
+ }
+ };
+
+ mailCopy.IsRead = true;
+
+ sut.UpdateFrom(mailCopy, MailCopyChangeFlags.IsRead);
+
+ raisedProperties.Should().Equal(nameof(MailItemViewModel.IsRead));
+ }
+
+ [Fact]
+ public void UpdateFrom_ShouldNotifyAddressAndDependentSenderFields_WhenFromAddressChanges()
+ {
+ var original = CreateMailCopy("thread-1", DateTime.UtcNow);
+ original.FromName = string.Empty;
+ var updated = CloneMailCopy(original);
+ updated.FromAddress = "updated@wino.dev";
+
+ var sut = new MailItemViewModel(original);
+ var raisedProperties = new List();
+
+ sut.PropertyChanged += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.PropertyName))
+ {
+ raisedProperties.Add(e.PropertyName);
+ }
+ };
+
+ sut.UpdateFrom(updated);
+
+ raisedProperties.Should().Equal(
+ nameof(MailItemViewModel.FromAddress),
+ nameof(MailItemViewModel.FromName),
+ nameof(MailItemViewModel.SortingName));
+ }
+
+ [Fact]
+ public async Task UpdateMailCopy_ShouldNotifyThreadOnlyForReadState_WhenReadStateChanges()
+ {
+ var collection = new WinoMailCollection
+ {
+ CoreDispatcher = new ImmediateDispatcher()
+ };
+
+ var older = CreateMailCopy("thread-1", DateTime.UtcNow.AddMinutes(-5));
+ var latest = CreateMailCopy("thread-1", DateTime.UtcNow);
+
+ await collection.AddAsync(older);
+ await collection.AddAsync(latest);
+
+ ThreadMailItemViewModel? threadItem = null;
+ foreach (var group in collection.MailItems)
+ {
+ foreach (var item in group)
+ {
+ if (item is ThreadMailItemViewModel thread)
+ {
+ threadItem = thread;
+ break;
+ }
+ }
+
+ if (threadItem != null)
+ break;
+ }
+
+ threadItem.Should().NotBeNull();
+
+ var raisedProperties = new List();
+ threadItem!.PropertyChanged += (_, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.PropertyName))
+ {
+ raisedProperties.Add(e.PropertyName);
+ }
+ };
+
+ latest.IsRead = true;
+
+ await collection.UpdateMailCopy(latest, MailUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead);
+
+ raisedProperties.Should().Equal(nameof(ThreadMailItemViewModel.IsRead));
+ }
+
+ private static MailCopy CreateMailCopy(string threadId, DateTime creationDate)
+ => new()
+ {
+ UniqueId = Guid.NewGuid(),
+ Id = Guid.NewGuid().ToString("N"),
+ FolderId = Guid.NewGuid(),
+ ThreadId = threadId,
+ MessageId = $"message-{Guid.NewGuid():N}",
+ References = string.Empty,
+ InReplyTo = string.Empty,
+ FromName = "Sender",
+ FromAddress = "sender@wino.dev",
+ Subject = "Subject",
+ PreviewText = "Preview",
+ CreationDate = creationDate,
+ Importance = MailImportance.Normal,
+ IsRead = false,
+ IsFlagged = false,
+ IsFocused = false,
+ HasAttachments = false,
+ ItemType = MailItemType.Mail,
+ DraftId = string.Empty,
+ IsDraft = false,
+ FileId = Guid.NewGuid()
+ };
+
+ private static MailCopy CloneMailCopy(MailCopy source)
+ => new()
+ {
+ UniqueId = source.UniqueId,
+ Id = source.Id,
+ FolderId = source.FolderId,
+ ThreadId = source.ThreadId,
+ MessageId = source.MessageId,
+ References = source.References,
+ InReplyTo = source.InReplyTo,
+ FromName = source.FromName,
+ FromAddress = source.FromAddress,
+ Subject = source.Subject,
+ PreviewText = source.PreviewText,
+ CreationDate = source.CreationDate,
+ Importance = source.Importance,
+ IsRead = source.IsRead,
+ IsFlagged = source.IsFlagged,
+ IsFocused = source.IsFocused,
+ HasAttachments = source.HasAttachments,
+ ItemType = source.ItemType,
+ DraftId = source.DraftId,
+ IsDraft = source.IsDraft,
+ FileId = source.FileId,
+ SenderContact = source.SenderContact,
+ AssignedAccount = source.AssignedAccount,
+ AssignedFolder = source.AssignedFolder
+ };
+
+ private sealed class ImmediateDispatcher : IDispatcher
+ {
+ public Task ExecuteOnUIThread(Action action)
+ {
+ action();
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs
index da9b4b35..2b378ecb 100644
--- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs
+++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs
@@ -753,21 +753,29 @@ public class WinoMailCollection : ObservableRecipient, IRecipient
/// Updated mail copy.
///
- public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource)
+ public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties = MailCopyChangeFlags.None)
{
return ExecuteUIThread(() =>
{
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
if (itemContainer == null) return;
+ MailCopyChangeFlags appliedChanges = MailCopyChangeFlags.None;
if (itemContainer.ItemViewModel != null)
{
UpdateUniqueIdHashes(itemContainer.ItemViewModel, false);
- // Update the MailCopy using UpdateFrom to properly notify all XAML bindings
- // This maintains reference integrity and ensures PropertyChanged is raised for all properties
- itemContainer.ItemViewModel.UpdateFrom(updatedMailCopy);
+ itemContainer.ThreadViewModel?.SuspendChildPropertyNotifications();
+
+ try
+ {
+ appliedChanges = itemContainer.ItemViewModel.UpdateFrom(updatedMailCopy, changedProperties);
+ }
+ finally
+ {
+ itemContainer.ThreadViewModel?.ResumeChildPropertyNotifications();
+ }
// Mark the item view model as busy until the network operation is completed.
itemContainer.ItemViewModel.IsBusy = mailUpdateSource == MailUpdateSource.ClientUpdated;
@@ -781,8 +789,10 @@ public class WinoMailCollection : ObservableRecipient, IRecipient
{
- CurrentMailDraftItem.UpdateFrom(updatedMail);
+ CurrentMailDraftItem.UpdateFrom(updatedMail, changedProperties);
await UpdatePendingOperationStateAsync();
NotifyComposeActionStateChanged();
});
diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs
index dce77d34..75af1f53 100644
--- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs
@@ -211,68 +211,200 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
}
}
- ///
- /// Updates the MailCopy with new data and notifies all bound properties.
- /// This method copies values from the source to the existing MailCopy to maintain reference integrity,
- /// then explicitly raises PropertyChanged for all dependent properties.
- ///
- /// The source MailCopy with updated values.
- public void UpdateFrom(MailCopy source)
+ public static MailCopyChangeFlags GetChangeFlagsForProperty(string propertyName)
{
- if (source == null) return;
+ return propertyName switch
+ {
+ nameof(CreationDate) or nameof(SortingDate) => MailCopyChangeFlags.CreationDate,
+ nameof(IsFlagged) => MailCopyChangeFlags.IsFlagged,
+ nameof(FromName) or nameof(SortingName) => MailCopyChangeFlags.FromName,
+ nameof(IsFocused) => MailCopyChangeFlags.IsFocused,
+ nameof(IsRead) => MailCopyChangeFlags.IsRead,
+ nameof(IsDraft) => MailCopyChangeFlags.IsDraft,
+ nameof(DraftId) => MailCopyChangeFlags.DraftId,
+ nameof(Id) => MailCopyChangeFlags.Id,
+ nameof(Subject) => MailCopyChangeFlags.Subject,
+ nameof(PreviewText) => MailCopyChangeFlags.PreviewText,
+ nameof(FromAddress) => MailCopyChangeFlags.FromAddress,
+ nameof(HasAttachments) => MailCopyChangeFlags.HasAttachments,
+ nameof(IsCalendarEvent) => MailCopyChangeFlags.ItemType,
+ nameof(Importance) => MailCopyChangeFlags.Importance,
+ nameof(ThreadId) => MailCopyChangeFlags.ThreadId,
+ nameof(MessageId) => MailCopyChangeFlags.MessageId,
+ nameof(References) => MailCopyChangeFlags.References,
+ nameof(InReplyTo) => MailCopyChangeFlags.InReplyTo,
+ nameof(FileId) => MailCopyChangeFlags.FileId,
+ nameof(FolderId) => MailCopyChangeFlags.FolderId,
+ nameof(UniqueId) => MailCopyChangeFlags.UniqueId,
+ nameof(Base64ContactPicture) or nameof(SenderContact) => MailCopyChangeFlags.SenderContact,
+ _ => MailCopyChangeFlags.None
+ };
+ }
- // Update the underlying MailCopy properties directly to maintain reference integrity
- // This is important because other parts of the app may hold references to this MailCopy
- // Note: UniqueId is the primary key and should match - we don't update it
- MailCopy.Id = source.Id;
- MailCopy.FolderId = source.FolderId;
- MailCopy.ThreadId = source.ThreadId;
- MailCopy.MessageId = source.MessageId;
- MailCopy.References = source.References;
- MailCopy.InReplyTo = source.InReplyTo;
- MailCopy.IsDraft = source.IsDraft;
- MailCopy.DraftId = source.DraftId;
- MailCopy.CreationDate = source.CreationDate;
- MailCopy.Subject = source.Subject;
- MailCopy.PreviewText = source.PreviewText;
- MailCopy.FromName = source.FromName;
- MailCopy.FromAddress = source.FromAddress;
- MailCopy.HasAttachments = source.HasAttachments;
- MailCopy.Importance = source.Importance;
- MailCopy.IsRead = source.IsRead;
- MailCopy.IsFlagged = source.IsFlagged;
- MailCopy.IsFocused = source.IsFocused;
- MailCopy.FileId = source.FileId;
- MailCopy.ItemType = source.ItemType;
- MailCopy.SenderContact = source.SenderContact;
- MailCopy.AssignedAccount = source.AssignedAccount;
- MailCopy.AssignedFolder = source.AssignedFolder;
+ ///
+ /// Updates the existing while raising only the relevant UI notifications.
+ ///
+ /// Source data used to update this item.
+ ///
+ /// Optional set of known changes. This is required when is the same instance
+ /// and has already been mutated by Apply/Revert flows.
+ ///
+ /// The effective set of changed fields used for notifications.
+ public MailCopyChangeFlags UpdateFrom(MailCopy source, MailCopyChangeFlags changeHint = MailCopyChangeFlags.None)
+ {
+ if (source == null) return MailCopyChangeFlags.None;
- // Raise PropertyChanged for all properties that XAML may bind to
- 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(IsCalendarEvent));
- 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(SenderContact));
- OnPropertyChanged(nameof(SortingDate));
- OnPropertyChanged(nameof(SortingName));
+ var changedFlags = MailCopyChangeFlags.None;
+ var isSameReference = ReferenceEquals(MailCopy, source);
+
+ if (!isSameReference)
+ {
+ changedFlags |= SetIfChanged(MailCopy.Id, source.Id, value => MailCopy.Id = value, MailCopyChangeFlags.Id);
+ changedFlags |= SetIfChanged(MailCopy.FolderId, source.FolderId, value => MailCopy.FolderId = value, MailCopyChangeFlags.FolderId);
+ changedFlags |= SetIfChanged(MailCopy.ThreadId, source.ThreadId, value => MailCopy.ThreadId = value, MailCopyChangeFlags.ThreadId);
+ changedFlags |= SetIfChanged(MailCopy.MessageId, source.MessageId, value => MailCopy.MessageId = value, MailCopyChangeFlags.MessageId);
+ changedFlags |= SetIfChanged(MailCopy.References, source.References, value => MailCopy.References = value, MailCopyChangeFlags.References);
+ changedFlags |= SetIfChanged(MailCopy.InReplyTo, source.InReplyTo, value => MailCopy.InReplyTo = value, MailCopyChangeFlags.InReplyTo);
+ changedFlags |= SetIfChanged(MailCopy.IsDraft, source.IsDraft, value => MailCopy.IsDraft = value, MailCopyChangeFlags.IsDraft);
+ changedFlags |= SetIfChanged(MailCopy.DraftId, source.DraftId, value => MailCopy.DraftId = value, MailCopyChangeFlags.DraftId);
+ changedFlags |= SetIfChanged(MailCopy.CreationDate, source.CreationDate, value => MailCopy.CreationDate = value, MailCopyChangeFlags.CreationDate);
+ changedFlags |= SetIfChanged(MailCopy.Subject, source.Subject, value => MailCopy.Subject = value, MailCopyChangeFlags.Subject);
+ changedFlags |= SetIfChanged(MailCopy.PreviewText, source.PreviewText, value => MailCopy.PreviewText = value, MailCopyChangeFlags.PreviewText);
+ changedFlags |= SetIfChanged(MailCopy.FromName, source.FromName, value => MailCopy.FromName = value, MailCopyChangeFlags.FromName);
+ changedFlags |= SetIfChanged(MailCopy.FromAddress, source.FromAddress, value => MailCopy.FromAddress = value, MailCopyChangeFlags.FromAddress);
+ changedFlags |= SetIfChanged(MailCopy.HasAttachments, source.HasAttachments, value => MailCopy.HasAttachments = value, MailCopyChangeFlags.HasAttachments);
+ changedFlags |= SetIfChanged(MailCopy.Importance, source.Importance, value => MailCopy.Importance = value, MailCopyChangeFlags.Importance);
+ changedFlags |= SetIfChanged(MailCopy.IsRead, source.IsRead, value => MailCopy.IsRead = value, MailCopyChangeFlags.IsRead);
+ changedFlags |= SetIfChanged(MailCopy.IsFlagged, source.IsFlagged, value => MailCopy.IsFlagged = value, MailCopyChangeFlags.IsFlagged);
+ changedFlags |= SetIfChanged(MailCopy.IsFocused, source.IsFocused, value => MailCopy.IsFocused = value, MailCopyChangeFlags.IsFocused);
+ changedFlags |= SetIfChanged(MailCopy.FileId, source.FileId, value => MailCopy.FileId = value, MailCopyChangeFlags.FileId);
+ changedFlags |= SetIfChanged(MailCopy.ItemType, source.ItemType, value => MailCopy.ItemType = value, MailCopyChangeFlags.ItemType);
+ changedFlags |= SetIfChanged(MailCopy.SenderContact, source.SenderContact, value => MailCopy.SenderContact = value, MailCopyChangeFlags.SenderContact);
+ changedFlags |= SetIfChanged(MailCopy.AssignedAccount, source.AssignedAccount, value => MailCopy.AssignedAccount = value, MailCopyChangeFlags.AssignedAccount);
+ changedFlags |= SetIfChanged(MailCopy.AssignedFolder, source.AssignedFolder, value => MailCopy.AssignedFolder = value, MailCopyChangeFlags.AssignedFolder);
+ changedFlags |= SetIfChanged(MailCopy.UniqueId, source.UniqueId, value => MailCopy.UniqueId = value, MailCopyChangeFlags.UniqueId);
+ }
+
+ changedFlags |= changeHint;
+
+ if (isSameReference && changedFlags == MailCopyChangeFlags.None)
+ {
+ // Without a hint there is no reliable way to diff in-place updates on the same instance.
+ // Fall back to full refresh to preserve correctness.
+ changedFlags = MailCopyChangeFlags.All;
+ }
+
+ RaisePropertyChanges(changedFlags);
+
+ return changedFlags;
+ }
+
+ private static MailCopyChangeFlags SetIfChanged(T currentValue, T newValue, Action setter, MailCopyChangeFlags flag)
+ {
+ if (EqualityComparer.Default.Equals(currentValue, newValue))
+ return MailCopyChangeFlags.None;
+
+ setter(newValue);
+ return flag;
+ }
+
+ private void RaisePropertyChanges(MailCopyChangeFlags changedFlags)
+ {
+ if (changedFlags == MailCopyChangeFlags.None)
+ return;
+
+ var changedProperties = new List(12);
+
+ void Queue(string propertyName)
+ {
+ if (!changedProperties.Contains(propertyName))
+ {
+ changedProperties.Add(propertyName);
+ }
+ }
+
+ if ((changedFlags & MailCopyChangeFlags.CreationDate) != 0)
+ {
+ Queue(nameof(CreationDate));
+ Queue(nameof(SortingDate));
+ }
+
+ if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0)
+ Queue(nameof(IsFlagged));
+
+ if ((changedFlags & MailCopyChangeFlags.FromName) != 0)
+ {
+ Queue(nameof(FromName));
+ Queue(nameof(SortingName));
+ }
+
+ if ((changedFlags & MailCopyChangeFlags.FromAddress) != 0)
+ {
+ Queue(nameof(FromAddress));
+ Queue(nameof(FromName));
+ Queue(nameof(SortingName));
+ }
+
+ if ((changedFlags & MailCopyChangeFlags.IsFocused) != 0)
+ Queue(nameof(IsFocused));
+
+ if ((changedFlags & MailCopyChangeFlags.IsRead) != 0)
+ Queue(nameof(IsRead));
+
+ if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0)
+ Queue(nameof(IsDraft));
+
+ if ((changedFlags & MailCopyChangeFlags.DraftId) != 0)
+ Queue(nameof(DraftId));
+
+ if ((changedFlags & MailCopyChangeFlags.Id) != 0)
+ Queue(nameof(Id));
+
+ if ((changedFlags & MailCopyChangeFlags.Subject) != 0)
+ Queue(nameof(Subject));
+
+ if ((changedFlags & MailCopyChangeFlags.PreviewText) != 0)
+ Queue(nameof(PreviewText));
+
+ if ((changedFlags & MailCopyChangeFlags.HasAttachments) != 0)
+ Queue(nameof(HasAttachments));
+
+ if ((changedFlags & MailCopyChangeFlags.ItemType) != 0)
+ Queue(nameof(IsCalendarEvent));
+
+ 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));
+ }
+
+ foreach (var changedProperty in changedProperties)
+ {
+ OnPropertyChanged(changedProperty);
+ }
}
}
diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs
index 62c8163f..c5d40d42 100644
--- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs
@@ -19,6 +19,7 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
private readonly string _threadId;
private readonly HashSet _uniqueIdSet = [];
private MailItemViewModel _cachedLatestMailViewModel;
+ private int _suspendChildPropertyNotificationsCount;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
@@ -202,6 +203,23 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
_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
///
@@ -225,9 +243,9 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
ThreadEmails.Insert(insertIndex, email);
email.PropertyChanged += ThreadEmailPropertyChanged;
_uniqueIdSet.Add(email.MailCopy.UniqueId);
- _cachedLatestMailViewModel = ThreadEmails[0];
+ RefreshLatestMailCache();
OnPropertyChanged(nameof(EmailCount));
- NotifyMailItemUpdated(email);
+ NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
}
///
@@ -239,9 +257,9 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
{
email.PropertyChanged -= ThreadEmailPropertyChanged;
_uniqueIdSet.Remove(email.MailCopy.UniqueId);
- _cachedLatestMailViewModel = ThreadEmails.Count > 0 ? ThreadEmails[0] : null;
+ RefreshLatestMailCache();
OnPropertyChanged(nameof(EmailCount));
- NotifyMailItemUpdated(email);
+ NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
}
}
@@ -256,51 +274,190 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
private void ThreadEmailPropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName == nameof(MailItemViewModel.IsSelected) || e.PropertyName == nameof(MailItemViewModel.IsDisplayedInThread))
+ if (_suspendChildPropertyNotificationsCount > 0)
return;
- if (e.PropertyName == nameof(MailItemViewModel.IsRead))
+ if (sender is not MailItemViewModel updatedMailItem)
+ return;
+
+ if (e.PropertyName == nameof(MailItemViewModel.IsSelected) ||
+ e.PropertyName == nameof(MailItemViewModel.IsDisplayedInThread) ||
+ e.PropertyName == nameof(MailItemViewModel.IsBusy))
{
- OnPropertyChanged(nameof(IsRead));
return;
}
- NotifyMailItemUpdated(sender as MailItemViewModel);
+ 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.
- /// 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)
+ /// Set of changed child fields.
+ public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem, MailCopyChangeFlags changedFlags = MailCopyChangeFlags.All)
{
- // 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(IsCalendarEvent));
- 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(SenderContact));
- OnPropertyChanged(nameof(ThumbnailUpdatedEvent));
- OnPropertyChanged(nameof(SortingDate));
- OnPropertyChanged(nameof(SortingName));
+ 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);
+ }
}
///
diff --git a/Wino.Mail.ViewModels/MailBaseViewModel.cs b/Wino.Mail.ViewModels/MailBaseViewModel.cs
index fb30d5df..07be737f 100644
--- a/Wino.Mail.ViewModels/MailBaseViewModel.cs
+++ b/Wino.Mail.ViewModels/MailBaseViewModel.cs
@@ -22,7 +22,7 @@ public class MailBaseViewModel : CoreBaseViewModel,
{
protected virtual void OnMailAdded(MailCopy addedMail) { }
protected virtual void OnMailRemoved(MailCopy removedMail) { }
- protected virtual void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source) { }
+ protected virtual void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties) { }
protected virtual void OnMailDownloaded(MailCopy downloadedMail) { }
protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { }
protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { }
@@ -33,7 +33,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, message.Source);
+ void IRecipient.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail, message.Source, message.ChangedProperties);
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 acc9136c..7ae1f4bd 100644
--- a/Wino.Mail.ViewModels/MailListPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs
@@ -599,6 +599,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false);
+ var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync().ConfigureAwait(false);
+ ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
await MailCollection.AddRangeAsync(viewModels, false);
await ExecuteUIThread(() => { IsInitializingFolder = false; });
@@ -792,9 +794,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
- protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source)
+ protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties)
{
- base.OnMailUpdated(updatedMail, source);
+ base.OnMailUpdated(updatedMail, source, changedProperties);
try
{
@@ -810,7 +812,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
return;
}
- await MailCollection.UpdateMailCopy(updatedMail, source);
+ await MailCollection.UpdateMailCopy(updatedMail, source, changedProperties);
}
finally
{
@@ -948,6 +950,48 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}, cancellationToken).ConfigureAwait(false);
}
+ private async Task> GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(CancellationToken cancellationToken = default)
+ {
+ var pendingOperationUniqueIds = new HashSet();
+
+ var accountIds = ActiveFolder?.HandlingFolders?
+ .Select(folder => folder.MailAccountId)
+ .Where(accountId => accountId != Guid.Empty)
+ .Distinct()
+ .ToList();
+
+ if (accountIds == null || accountIds.Count == 0)
+ return pendingOperationUniqueIds;
+
+ foreach (var accountId in accountIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
+
+ if (synchronizer == null)
+ continue;
+
+ foreach (var uniqueId in synchronizer.GetPendingOperationUniqueIds())
+ {
+ pendingOperationUniqueIds.Add(uniqueId);
+ }
+ }
+
+ return pendingOperationUniqueIds;
+ }
+
+ private static void ApplyPendingOperationBusyStates(IEnumerable viewModels, HashSet pendingOperationUniqueIds)
+ {
+ if (viewModels == null || pendingOperationUniqueIds == null || pendingOperationUniqueIds.Count == 0)
+ return;
+
+ foreach (var viewModel in viewModels)
+ {
+ viewModel.IsBusy = pendingOperationUniqueIds.Contains(viewModel.MailCopy.UniqueId);
+ }
+ }
+
[RelayCommand]
private async Task PerformOnlineSearchAsync()
{
@@ -1076,6 +1120,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Just create VMs and do bulk insert.
var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false);
+ var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(cancellationToken).ConfigureAwait(false);
+ ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
index 99e3644a..3115a52e 100644
--- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
@@ -651,9 +651,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
}
- protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source)
+ protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties)
{
- base.OnMailUpdated(updatedMail, source);
+ base.OnMailUpdated(updatedMail, source, changedProperties);
if (initializedMailItemViewModel == null) return;
diff --git a/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs b/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs
index 0e1abe04..959a6f78 100644
--- a/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs
+++ b/Wino.Mail.WinUI/Controls/ImagePreviewControl.cs
@@ -95,6 +95,8 @@ public sealed partial class ImagePreviewControl : PersonPicture
if (string.IsNullOrEmpty(e.PropertyName)
|| e.PropertyName == nameof(IMailItemDisplayInformation.Base64ContactPicture)
|| e.PropertyName == nameof(IMailItemDisplayInformation.SenderContact)
+ || e.PropertyName == nameof(IMailItemDisplayInformation.FromName)
+ || e.PropertyName == nameof(IMailItemDisplayInformation.FromAddress)
|| e.PropertyName == nameof(IMailItemDisplayInformation.ThumbnailUpdatedEvent))
{
RequestRefresh();
diff --git a/Wino.Mail.WinUI/Services/NotificationBuilder.cs b/Wino.Mail.WinUI/Services/NotificationBuilder.cs
index 3d1517e6..372c9625 100644
--- a/Wino.Mail.WinUI/Services/NotificationBuilder.cs
+++ b/Wino.Mail.WinUI/Services/NotificationBuilder.cs
@@ -243,6 +243,10 @@ public class NotificationBuilder : INotificationBuilder
{
ToastNotificationManager.History.Remove(mailUniqueId.ToString(), null);
}
+ catch (ArgumentException)
+ {
+ // Notification does not exists. Ignore.
+ }
catch (Exception ex)
{
Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}");
diff --git a/Wino.Messages/UI/MailUpdatedMessage.cs b/Wino.Messages/UI/MailUpdatedMessage.cs
index 546a8d9d..66c4161b 100644
--- a/Wino.Messages/UI/MailUpdatedMessage.cs
+++ b/Wino.Messages/UI/MailUpdatedMessage.cs
@@ -3,4 +3,4 @@ using Wino.Core.Domain.Enums;
namespace Wino.Messaging.UI;
-public record MailUpdatedMessage(MailCopy UpdatedMail, MailUpdateSource Source) : UIMessageBase;
+public record MailUpdatedMessage(MailCopy UpdatedMail, MailUpdateSource Source, MailCopyChangeFlags ChangedProperties = MailCopyChangeFlags.None) : UIMessageBase;