Property change based updates on the mails for fast bulk operations.

This commit is contained in:
Burak Kaan Köse
2026-03-01 12:07:15 +01:00
parent 11158fe737
commit 211faff750
17 changed files with 711 additions and 121 deletions
@@ -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
}
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -30,6 +31,11 @@ public interface IBaseSynchronizer
/// <param name="mailUniqueId">Mail unique id to check.</param> /// <param name="mailUniqueId">Mail unique id to check.</param>
bool HasPendingOperation(Guid mailUniqueId); bool HasPendingOperation(Guid mailUniqueId);
/// <summary>
/// Returns mail unique ids that currently have queued or executing operations.
/// </summary>
IReadOnlyCollection<Guid> GetPendingOperationUniqueIds();
/// <summary> /// <summary>
/// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id. /// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
/// </summary> /// </summary>
@@ -20,7 +20,7 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> Mail
item.IsRead = true; 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<MailCopy> Mail
item.IsRead = false; item.IsRead = false;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientReverted)); WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
} }
} }
+2 -2
View File
@@ -31,7 +31,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
Item.IsFlagged = IsFlagged; 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() public override void RevertUIChanges()
@@ -41,7 +41,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
Item.IsFlagged = !IsFlagged; Item.IsFlagged = !IsFlagged;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted)); WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged));
} }
} }
+2 -2
View File
@@ -30,7 +30,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
Item.IsRead = IsRead; 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() public override void RevertUIChanges()
@@ -40,7 +40,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
Item.IsRead = !IsRead; Item.IsRead = !IsRead;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted)); WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
} }
} }
@@ -2,6 +2,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -130,6 +131,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId); public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId);
public IReadOnlyCollection<Guid> GetPendingOperationUniqueIds() => _pendingMailOperationIds.Keys.ToArray();
public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId); public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId);
protected void TrackQueuedRequest(IRequestBase request) protected void TrackQueuedRequest(IRequestBase request)
@@ -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<string>();
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<string>();
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<string>();
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;
}
}
}
@@ -753,21 +753,29 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
/// </summary> /// </summary>
/// <param name="updatedMailCopy">Updated mail copy.</param> /// <param name="updatedMailCopy">Updated mail copy.</param>
/// <returns></returns> /// <returns></returns>
public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource) public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties = MailCopyChangeFlags.None)
{ {
return ExecuteUIThread(() => return ExecuteUIThread(() =>
{ {
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId); var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
if (itemContainer == null) return; if (itemContainer == null) return;
MailCopyChangeFlags appliedChanges = MailCopyChangeFlags.None;
if (itemContainer.ItemViewModel != null) if (itemContainer.ItemViewModel != null)
{ {
UpdateUniqueIdHashes(itemContainer.ItemViewModel, false); UpdateUniqueIdHashes(itemContainer.ItemViewModel, false);
// Update the MailCopy using UpdateFrom to properly notify all XAML bindings itemContainer.ThreadViewModel?.SuspendChildPropertyNotifications();
// This maintains reference integrity and ensures PropertyChanged is raised for all properties
itemContainer.ItemViewModel.UpdateFrom(updatedMailCopy); try
{
appliedChanges = itemContainer.ItemViewModel.UpdateFrom(updatedMailCopy, changedProperties);
}
finally
{
itemContainer.ThreadViewModel?.ResumeChildPropertyNotifications();
}
// Mark the item view model as busy until the network operation is completed. // Mark the item view model as busy until the network operation is completed.
itemContainer.ItemViewModel.IsBusy = mailUpdateSource == MailUpdateSource.ClientUpdated; itemContainer.ItemViewModel.IsBusy = mailUpdateSource == MailUpdateSource.ClientUpdated;
@@ -781,8 +789,10 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
} }
} }
// Trigger thread property notifications if this item is in a thread if (itemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
itemContainer.ThreadViewModel?.NotifyMailItemUpdated(itemContainer.ItemViewModel); {
itemContainer.ThreadViewModel.NotifyMailItemUpdated(itemContainer.ItemViewModel, appliedChanges);
}
}); });
} }
+3 -3
View File
@@ -840,9 +840,9 @@ public partial class ComposePageViewModel : MailBaseViewModel,
_dialogService.InfoBarMessage(Translator.Info_InvalidAddressTitle, string.Format(Translator.Info_InvalidAddressMessage, address), InfoBarMessageType.Warning); _dialogService.InfoBarMessage(Translator.Info_InvalidAddressTitle, string.Format(Translator.Info_InvalidAddressMessage, address), InfoBarMessageType.Warning);
} }
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 (CurrentMailDraftItem == null) return; if (CurrentMailDraftItem == null) return;
@@ -850,7 +850,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
{ {
await ExecuteUIThread(async () => await ExecuteUIThread(async () =>
{ {
CurrentMailDraftItem.UpdateFrom(updatedMail); CurrentMailDraftItem.UpdateFrom(updatedMail, changedProperties);
await UpdatePendingOperationStateAsync(); await UpdatePendingOperationStateAsync();
NotifyComposeActionStateChanged(); NotifyComposeActionStateChanged();
}); });
+192 -60
View File
@@ -211,68 +211,200 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
} }
} }
/// <summary> public static MailCopyChangeFlags GetChangeFlagsForProperty(string propertyName)
/// 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.
/// </summary>
/// <param name="source">The source MailCopy with updated values.</param>
public void UpdateFrom(MailCopy source)
{ {
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 /// <summary>
// This is important because other parts of the app may hold references to this MailCopy /// Updates the existing <see cref="MailCopy"/> while raising only the relevant UI notifications.
// Note: UniqueId is the primary key and should match - we don't update it /// </summary>
MailCopy.Id = source.Id; /// <param name="source">Source data used to update this item.</param>
MailCopy.FolderId = source.FolderId; /// <param name="changeHint">
MailCopy.ThreadId = source.ThreadId; /// Optional set of known changes. This is required when <paramref name="source"/> is the same instance
MailCopy.MessageId = source.MessageId; /// and has already been mutated by Apply/Revert flows.
MailCopy.References = source.References; /// </param>
MailCopy.InReplyTo = source.InReplyTo; /// <returns>The effective set of changed fields used for notifications.</returns>
MailCopy.IsDraft = source.IsDraft; public MailCopyChangeFlags UpdateFrom(MailCopy source, MailCopyChangeFlags changeHint = MailCopyChangeFlags.None)
MailCopy.DraftId = source.DraftId; {
MailCopy.CreationDate = source.CreationDate; if (source == null) return MailCopyChangeFlags.None;
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;
// Raise PropertyChanged for all properties that XAML may bind to var changedFlags = MailCopyChangeFlags.None;
OnPropertyChanged(nameof(CreationDate)); var isSameReference = ReferenceEquals(MailCopy, source);
OnPropertyChanged(nameof(IsFlagged));
OnPropertyChanged(nameof(FromName)); if (!isSameReference)
OnPropertyChanged(nameof(IsFocused)); {
OnPropertyChanged(nameof(IsRead)); changedFlags |= SetIfChanged(MailCopy.Id, source.Id, value => MailCopy.Id = value, MailCopyChangeFlags.Id);
OnPropertyChanged(nameof(IsDraft)); changedFlags |= SetIfChanged(MailCopy.FolderId, source.FolderId, value => MailCopy.FolderId = value, MailCopyChangeFlags.FolderId);
OnPropertyChanged(nameof(DraftId)); changedFlags |= SetIfChanged(MailCopy.ThreadId, source.ThreadId, value => MailCopy.ThreadId = value, MailCopyChangeFlags.ThreadId);
OnPropertyChanged(nameof(Id)); changedFlags |= SetIfChanged(MailCopy.MessageId, source.MessageId, value => MailCopy.MessageId = value, MailCopyChangeFlags.MessageId);
OnPropertyChanged(nameof(Subject)); changedFlags |= SetIfChanged(MailCopy.References, source.References, value => MailCopy.References = value, MailCopyChangeFlags.References);
OnPropertyChanged(nameof(PreviewText)); changedFlags |= SetIfChanged(MailCopy.InReplyTo, source.InReplyTo, value => MailCopy.InReplyTo = value, MailCopyChangeFlags.InReplyTo);
OnPropertyChanged(nameof(FromAddress)); changedFlags |= SetIfChanged(MailCopy.IsDraft, source.IsDraft, value => MailCopy.IsDraft = value, MailCopyChangeFlags.IsDraft);
OnPropertyChanged(nameof(HasAttachments)); changedFlags |= SetIfChanged(MailCopy.DraftId, source.DraftId, value => MailCopy.DraftId = value, MailCopyChangeFlags.DraftId);
OnPropertyChanged(nameof(IsCalendarEvent)); changedFlags |= SetIfChanged(MailCopy.CreationDate, source.CreationDate, value => MailCopy.CreationDate = value, MailCopyChangeFlags.CreationDate);
OnPropertyChanged(nameof(Importance)); changedFlags |= SetIfChanged(MailCopy.Subject, source.Subject, value => MailCopy.Subject = value, MailCopyChangeFlags.Subject);
OnPropertyChanged(nameof(ThreadId)); changedFlags |= SetIfChanged(MailCopy.PreviewText, source.PreviewText, value => MailCopy.PreviewText = value, MailCopyChangeFlags.PreviewText);
OnPropertyChanged(nameof(MessageId)); changedFlags |= SetIfChanged(MailCopy.FromName, source.FromName, value => MailCopy.FromName = value, MailCopyChangeFlags.FromName);
OnPropertyChanged(nameof(References)); changedFlags |= SetIfChanged(MailCopy.FromAddress, source.FromAddress, value => MailCopy.FromAddress = value, MailCopyChangeFlags.FromAddress);
OnPropertyChanged(nameof(InReplyTo)); changedFlags |= SetIfChanged(MailCopy.HasAttachments, source.HasAttachments, value => MailCopy.HasAttachments = value, MailCopyChangeFlags.HasAttachments);
OnPropertyChanged(nameof(FileId)); changedFlags |= SetIfChanged(MailCopy.Importance, source.Importance, value => MailCopy.Importance = value, MailCopyChangeFlags.Importance);
OnPropertyChanged(nameof(FolderId)); changedFlags |= SetIfChanged(MailCopy.IsRead, source.IsRead, value => MailCopy.IsRead = value, MailCopyChangeFlags.IsRead);
OnPropertyChanged(nameof(UniqueId)); changedFlags |= SetIfChanged(MailCopy.IsFlagged, source.IsFlagged, value => MailCopy.IsFlagged = value, MailCopyChangeFlags.IsFlagged);
OnPropertyChanged(nameof(Base64ContactPicture)); changedFlags |= SetIfChanged(MailCopy.IsFocused, source.IsFocused, value => MailCopy.IsFocused = value, MailCopyChangeFlags.IsFocused);
OnPropertyChanged(nameof(SenderContact)); changedFlags |= SetIfChanged(MailCopy.FileId, source.FileId, value => MailCopy.FileId = value, MailCopyChangeFlags.FileId);
OnPropertyChanged(nameof(SortingDate)); changedFlags |= SetIfChanged(MailCopy.ItemType, source.ItemType, value => MailCopy.ItemType = value, MailCopyChangeFlags.ItemType);
OnPropertyChanged(nameof(SortingName)); 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>(T currentValue, T newValue, Action<T> setter, MailCopyChangeFlags flag)
{
if (EqualityComparer<T>.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<string>(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);
}
} }
} }
@@ -19,6 +19,7 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
private readonly string _threadId; private readonly string _threadId;
private readonly HashSet<Guid> _uniqueIdSet = []; private readonly HashSet<Guid> _uniqueIdSet = [];
private MailItemViewModel _cachedLatestMailViewModel; private MailItemViewModel _cachedLatestMailViewModel;
private int _suspendChildPropertyNotificationsCount;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedRecipients] [NotifyPropertyChangedRecipients]
@@ -202,6 +203,23 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
_threadId = 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();
}
/// <summary> /// <summary>
/// Adds an email to this thread /// Adds an email to this thread
/// </summary> /// </summary>
@@ -225,9 +243,9 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
ThreadEmails.Insert(insertIndex, email); ThreadEmails.Insert(insertIndex, email);
email.PropertyChanged += ThreadEmailPropertyChanged; email.PropertyChanged += ThreadEmailPropertyChanged;
_uniqueIdSet.Add(email.MailCopy.UniqueId); _uniqueIdSet.Add(email.MailCopy.UniqueId);
_cachedLatestMailViewModel = ThreadEmails[0]; RefreshLatestMailCache();
OnPropertyChanged(nameof(EmailCount)); OnPropertyChanged(nameof(EmailCount));
NotifyMailItemUpdated(email); NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
} }
/// <summary> /// <summary>
@@ -239,9 +257,9 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
{ {
email.PropertyChanged -= ThreadEmailPropertyChanged; email.PropertyChanged -= ThreadEmailPropertyChanged;
_uniqueIdSet.Remove(email.MailCopy.UniqueId); _uniqueIdSet.Remove(email.MailCopy.UniqueId);
_cachedLatestMailViewModel = ThreadEmails.Count > 0 ? ThreadEmails[0] : null; RefreshLatestMailCache();
OnPropertyChanged(nameof(EmailCount)); 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) private void ThreadEmailPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(MailItemViewModel.IsSelected) || e.PropertyName == nameof(MailItemViewModel.IsDisplayedInThread)) if (_suspendChildPropertyNotificationsCount > 0)
return; 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; 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);
} }
/// <summary> /// <summary>
/// Notifies that a mail item within this thread has been updated. /// 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.
/// </summary> /// </summary>
/// <param name="updatedMailItem">The mail item that was updated (can be null to refresh all).</param> /// <param name="updatedMailItem">The mail item that was updated (can be null to refresh all).</param>
public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem) /// <param name="changedFlags">Set of changed child fields.</param>
public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem, MailCopyChangeFlags changedFlags = MailCopyChangeFlags.All)
{ {
// Raise PropertyChanged for all computed properties that depend on ThreadEmails contents if (changedFlags == MailCopyChangeFlags.None)
OnPropertyChanged(nameof(Subject)); return;
OnPropertyChanged(nameof(FromName));
OnPropertyChanged(nameof(CreationDate)); var previousLatest = latestMailViewModel;
OnPropertyChanged(nameof(FromAddress));
OnPropertyChanged(nameof(PreviewText)); if (changedFlags == MailCopyChangeFlags.All ||
OnPropertyChanged(nameof(HasAttachments)); (changedFlags & MailCopyChangeFlags.CreationDate) != 0 ||
OnPropertyChanged(nameof(IsCalendarEvent)); previousLatest == null ||
OnPropertyChanged(nameof(IsFlagged)); !ThreadEmails.Contains(previousLatest))
OnPropertyChanged(nameof(IsFocused)); {
OnPropertyChanged(nameof(IsRead)); RefreshLatestMailCache();
OnPropertyChanged(nameof(IsDraft)); }
OnPropertyChanged(nameof(DraftId));
OnPropertyChanged(nameof(Id)); var currentLatest = latestMailViewModel;
OnPropertyChanged(nameof(Importance)); var latestChanged = !ReferenceEquals(previousLatest, currentLatest);
OnPropertyChanged(nameof(ThreadId));
OnPropertyChanged(nameof(MessageId)); var updatesDisplayedLatest = changedFlags == MailCopyChangeFlags.All ||
OnPropertyChanged(nameof(References)); updatedMailItem == null ||
OnPropertyChanged(nameof(InReplyTo)); latestChanged ||
OnPropertyChanged(nameof(FileId)); ReferenceEquals(updatedMailItem, previousLatest) ||
OnPropertyChanged(nameof(FolderId)); ReferenceEquals(updatedMailItem, currentLatest);
OnPropertyChanged(nameof(UniqueId));
OnPropertyChanged(nameof(Base64ContactPicture)); var changedProperties = new List<string>(10);
OnPropertyChanged(nameof(SenderContact));
OnPropertyChanged(nameof(ThumbnailUpdatedEvent)); void Queue(string propertyName)
OnPropertyChanged(nameof(SortingDate)); {
OnPropertyChanged(nameof(SortingName)); 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);
}
} }
/// <summary> /// <summary>
+2 -2
View File
@@ -22,7 +22,7 @@ public class MailBaseViewModel : CoreBaseViewModel,
{ {
protected virtual void OnMailAdded(MailCopy addedMail) { } protected virtual void OnMailAdded(MailCopy addedMail) { }
protected virtual void OnMailRemoved(MailCopy removedMail) { } 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 OnMailDownloaded(MailCopy downloadedMail) { }
protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { } protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { }
protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { } protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { }
@@ -33,7 +33,7 @@ public class MailBaseViewModel : CoreBaseViewModel,
void IRecipient<MailAddedMessage>.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail); void IRecipient<MailAddedMessage>.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail);
void IRecipient<MailRemovedMessage>.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail); void IRecipient<MailRemovedMessage>.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail);
void IRecipient<MailUpdatedMessage>.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail, message.Source); void IRecipient<MailUpdatedMessage>.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail, message.Source, message.ChangedProperties);
void IRecipient<MailDownloadedMessage>.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail); void IRecipient<MailDownloadedMessage>.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail);
void IRecipient<DraftMapped>.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId); void IRecipient<DraftMapped>.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId);
+49 -3
View File
@@ -599,6 +599,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
} }
var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false); var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false);
var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync().ConfigureAwait(false);
ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
await MailCollection.AddRangeAsync(viewModels, false); await MailCollection.AddRangeAsync(viewModels, false);
await ExecuteUIThread(() => { IsInitializingFolder = 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 try
{ {
@@ -810,7 +812,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
return; return;
} }
await MailCollection.UpdateMailCopy(updatedMail, source); await MailCollection.UpdateMailCopy(updatedMail, source, changedProperties);
} }
finally finally
{ {
@@ -948,6 +950,48 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}, cancellationToken).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false);
} }
private async Task<HashSet<Guid>> GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(CancellationToken cancellationToken = default)
{
var pendingOperationUniqueIds = new HashSet<Guid>();
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<MailItemViewModel> viewModels, HashSet<Guid> pendingOperationUniqueIds)
{
if (viewModels == null || pendingOperationUniqueIds == null || pendingOperationUniqueIds.Count == 0)
return;
foreach (var viewModel in viewModels)
{
viewModel.IsBusy = pendingOperationUniqueIds.Contains(viewModel.MailCopy.UniqueId);
}
}
[RelayCommand] [RelayCommand]
private async Task PerformOnlineSearchAsync() private async Task PerformOnlineSearchAsync()
{ {
@@ -1076,6 +1120,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Just create VMs and do bulk insert. // Just create VMs and do bulk insert.
var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false); var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false);
var pendingOperationUniqueIds = await GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(cancellationToken).ConfigureAwait(false);
ApplyPendingOperationBusyStates(viewModels, pendingOperationUniqueIds);
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true); await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
@@ -651,9 +651,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false)); 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; if (initializedMailItemViewModel == null) return;
@@ -95,6 +95,8 @@ public sealed partial class ImagePreviewControl : PersonPicture
if (string.IsNullOrEmpty(e.PropertyName) if (string.IsNullOrEmpty(e.PropertyName)
|| e.PropertyName == nameof(IMailItemDisplayInformation.Base64ContactPicture) || e.PropertyName == nameof(IMailItemDisplayInformation.Base64ContactPicture)
|| e.PropertyName == nameof(IMailItemDisplayInformation.SenderContact) || e.PropertyName == nameof(IMailItemDisplayInformation.SenderContact)
|| e.PropertyName == nameof(IMailItemDisplayInformation.FromName)
|| e.PropertyName == nameof(IMailItemDisplayInformation.FromAddress)
|| e.PropertyName == nameof(IMailItemDisplayInformation.ThumbnailUpdatedEvent)) || e.PropertyName == nameof(IMailItemDisplayInformation.ThumbnailUpdatedEvent))
{ {
RequestRefresh(); RequestRefresh();
@@ -243,6 +243,10 @@ public class NotificationBuilder : INotificationBuilder
{ {
ToastNotificationManager.History.Remove(mailUniqueId.ToString(), null); ToastNotificationManager.History.Remove(mailUniqueId.ToString(), null);
} }
catch (ArgumentException)
{
// Notification does not exists. Ignore.
}
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}"); Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}");
+1 -1
View File
@@ -3,4 +3,4 @@ using Wino.Core.Domain.Enums;
namespace Wino.Messaging.UI; namespace Wino.Messaging.UI;
public record MailUpdatedMessage(MailCopy UpdatedMail, MailUpdateSource Source) : UIMessageBase<MailUpdatedMessage>; public record MailUpdatedMessage(MailCopy UpdatedMail, MailUpdateSource Source, MailCopyChangeFlags ChangedProperties = MailCopyChangeFlags.None) : UIMessageBase<MailUpdatedMessage>;