Property change based updates on the mails for fast bulk operations.
This commit is contained in:
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user