Fixing UI thread issues with bulk operations and request queue refactoring.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -14,18 +15,26 @@ public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDe
|
||||
public bool ExcludeMustHaveFolders => false;
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
foreach (var item in MailsToDelete)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(item, EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
var removedMails = MailsToDelete?
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
|
||||
if (removedMails == null || removedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
foreach (var item in MailsToDelete)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new MailAddedMessage(item, EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
var addedMails = MailsToDelete?
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
|
||||
if (addedMails == null || addedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
|
||||
public List<Guid> SynchronizationFolderIds => [Folder.Id];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -17,31 +18,31 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> Mail
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
if (MailsToMarkRead == null || MailsToMarkRead.Count == 0) return;
|
||||
var updatedMails = MailsToMarkRead?
|
||||
.Where(item => item != null && !item.IsRead)
|
||||
.Select(item => new MailStateChange(item.UniqueId, IsRead: true))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in MailsToMarkRead)
|
||||
{
|
||||
// Skip if already read
|
||||
if (item.IsRead) continue;
|
||||
if (updatedMails == null || updatedMails.Count == 0)
|
||||
return;
|
||||
|
||||
item.IsRead = true;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, EntityUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead));
|
||||
}
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
|
||||
updatedMails,
|
||||
EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
if (MailsToMarkRead == null || MailsToMarkRead.Count == 0) return;
|
||||
var updatedMails = MailsToMarkRead?
|
||||
.Where(item => item != null && !item.IsRead)
|
||||
.Select(item => new MailStateChange(item.UniqueId, IsRead: false))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in MailsToMarkRead)
|
||||
{
|
||||
// Skip if already unread (wasn't changed by ApplyUIChanges)
|
||||
if (!item.IsRead) continue;
|
||||
if (updatedMails == null || updatedMails.Count == 0)
|
||||
return;
|
||||
|
||||
item.IsRead = false;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, EntityUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
|
||||
}
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
|
||||
updatedMails,
|
||||
EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -38,6 +39,7 @@ public record ArchiveRequest(bool IsArchiving, MailCopy Item, MailItemFolder Fro
|
||||
}
|
||||
|
||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Archive;
|
||||
public override object GroupingKey() => (Operation, IsArchiving, FromFolder.Id, ToFolder?.Id ?? Guid.Empty);
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
@@ -55,4 +57,22 @@ public class BatchArchiveRequest : BatchCollection<ArchiveRequest>
|
||||
public BatchArchiveRequest(IEnumerable<ArchiveRequest> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (removedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (addedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -25,25 +26,26 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
|
||||
/// If the mail is already in the desired flagged state, no change is needed.
|
||||
/// </summary>
|
||||
public bool IsNoOp { get; } = Item.IsFlagged == IsFlagged;
|
||||
public bool OriginalIsFlagged => _originalIsFlagged;
|
||||
|
||||
public override object GroupingKey() => (Operation, Item.FolderId, IsFlagged);
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
// Skip UI update if the mail is already in the desired state
|
||||
if (IsNoOp) return;
|
||||
|
||||
Item.IsFlagged = IsFlagged;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientUpdated, MailCopyChangeFlags.IsFlagged));
|
||||
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
|
||||
new MailStateChange(Item.UniqueId, IsFlagged: IsFlagged),
|
||||
EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
// Skip UI revert if this was a no-op request
|
||||
if (IsNoOp) return;
|
||||
|
||||
Item.IsFlagged = _originalIsFlagged;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged));
|
||||
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
|
||||
new MailStateChange(Item.UniqueId, IsFlagged: _originalIsFlagged),
|
||||
EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +54,34 @@ public class BatchChangeFlagRequest : BatchCollection<ChangeFlagRequest>
|
||||
public BatchChangeFlagRequest(IEnumerable<ChangeFlagRequest> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
var updatedMails = this
|
||||
.Where(x => !x.IsNoOp)
|
||||
.Select(x => new MailStateChange(x.Item.UniqueId, IsFlagged: x.IsFlagged))
|
||||
.ToList();
|
||||
|
||||
if (updatedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
|
||||
updatedMails,
|
||||
EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
var updatedMails = this
|
||||
.Where(x => !x.IsNoOp)
|
||||
.Select(x => new MailStateChange(x.Item.UniqueId, IsFlagged: x.OriginalIsFlagged))
|
||||
.ToList();
|
||||
|
||||
if (updatedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
|
||||
updatedMails,
|
||||
EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -30,6 +31,7 @@ public record ChangeJunkStateRequest(bool IsJunk, MailCopy Item, MailItemFolder
|
||||
public bool ExcludeMustHaveFolders => false;
|
||||
|
||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeJunkState;
|
||||
public override object GroupingKey() => (Operation, IsJunk, FromFolder.Id, TargetFolder?.Id ?? Guid.Empty);
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
@@ -47,4 +49,22 @@ public class BatchChangeJunkStateRequest : BatchCollection<ChangeJunkStateReques
|
||||
public BatchChangeJunkStateRequest(IEnumerable<ChangeJunkStateRequest> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (removedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (addedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -19,6 +20,7 @@ public record DeleteRequest(MailCopy MailItem) : MailRequestBase(MailItem),
|
||||
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
||||
public bool ExcludeMustHaveFolders => false;
|
||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Delete;
|
||||
public override object GroupingKey() => (Operation, Item.FolderId);
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
@@ -36,4 +38,22 @@ public class BatchDeleteRequest : BatchCollection<DeleteRequest>
|
||||
public BatchDeleteRequest(IEnumerable<DeleteRequest> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (removedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (addedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -24,25 +25,26 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
|
||||
/// If the mail is already in the desired read state, no change is needed.
|
||||
/// </summary>
|
||||
public bool IsNoOp { get; } = Item.IsRead == IsRead;
|
||||
public bool OriginalIsRead => _originalIsRead;
|
||||
|
||||
public override object GroupingKey() => (Operation, Item.FolderId, IsRead);
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
// Skip UI update if the mail is already in the desired state
|
||||
if (IsNoOp) return;
|
||||
|
||||
Item.IsRead = IsRead;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead));
|
||||
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
|
||||
new MailStateChange(Item.UniqueId, IsRead: IsRead),
|
||||
EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
// Skip UI revert if this was a no-op request
|
||||
if (IsNoOp) return;
|
||||
|
||||
Item.IsRead = _originalIsRead;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
|
||||
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
|
||||
new MailStateChange(Item.UniqueId, IsRead: _originalIsRead),
|
||||
EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,4 +53,34 @@ public class BatchMarkReadRequest : BatchCollection<MarkReadRequest>
|
||||
public BatchMarkReadRequest(IEnumerable<MarkReadRequest> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
var updatedMails = this
|
||||
.Where(x => !x.IsNoOp)
|
||||
.Select(x => new MailStateChange(x.Item.UniqueId, IsRead: x.IsRead))
|
||||
.ToList();
|
||||
|
||||
if (updatedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
|
||||
updatedMails,
|
||||
EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
var updatedMails = this
|
||||
.Where(x => !x.IsNoOp)
|
||||
.Select(x => new MailStateChange(x.Item.UniqueId, IsRead: x.OriginalIsRead))
|
||||
.ToList();
|
||||
|
||||
if (updatedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
|
||||
updatedMails,
|
||||
EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -15,6 +16,7 @@ public record MoveRequest(MailCopy Item, MailItemFolder FromFolder, MailItemFold
|
||||
public List<Guid> SynchronizationFolderIds => new() { FromFolder.Id, ToFolder.Id };
|
||||
public bool ExcludeMustHaveFolders => false;
|
||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Move;
|
||||
public override object GroupingKey() => (Operation, FromFolder.Id, ToFolder?.Id ?? Guid.Empty);
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
@@ -32,4 +34,22 @@ public class BatchMoveRequest : BatchCollection<MoveRequest>, IUIChangeRequest
|
||||
public BatchMoveRequest(IEnumerable<MoveRequest> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (removedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
|
||||
if (addedMails.Count == 0)
|
||||
return;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,25 +259,48 @@ public class SynchronizationManager : ISynchronizationManager, IRecipient<Accoun
|
||||
/// <param name="accountId">Account ID to queue the request for</param>
|
||||
/// <param name="triggerSynchronization">Whether to automatically trigger synchronization after queuing the request</param>
|
||||
public async Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization)
|
||||
=> await QueueRequestsAsync([request], accountId, triggerSynchronization).ConfigureAwait(false);
|
||||
|
||||
public async Task QueueRequestsAsync(IEnumerable<IRequestBase> requests, Guid accountId, bool triggerSynchronization)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
var requestList = requests?.Where(request => request != null).ToList() ?? [];
|
||||
if (requestList.Count == 0)
|
||||
return;
|
||||
|
||||
var synchronizer = await GetOrCreateSynchronizerAsync(accountId);
|
||||
if (synchronizer == null)
|
||||
{
|
||||
_logger.Error("Could not find or create synchronizer for account {AccountId} to queue request", accountId);
|
||||
_logger.Error("Could not find or create synchronizer for account {AccountId} to queue {RequestCount} request(s)", accountId, requestList.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
|
||||
request.GetType().Name, accountId);
|
||||
if (requestList.Count == 1)
|
||||
{
|
||||
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
|
||||
requestList[0].GetType().Name, accountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
var requestSummary = string.Join(", ", requestList
|
||||
.GroupBy(request => request.GetType().Name)
|
||||
.OrderBy(group => group.Key)
|
||||
.Select(group => $"{group.Key} x{group.Count()}"));
|
||||
|
||||
synchronizer.QueueRequest(request);
|
||||
_logger.Debug("Queuing {RequestCount} requests for account {AccountId}: {RequestSummary}",
|
||||
requestList.Count, accountId, requestSummary);
|
||||
}
|
||||
|
||||
foreach (var request in requestList)
|
||||
{
|
||||
synchronizer.QueueRequest(request);
|
||||
}
|
||||
|
||||
if (triggerSynchronization)
|
||||
{
|
||||
// Determine if this is a calendar or mail operation
|
||||
bool isCalendarOperation = request is ICalendarActionRequest;
|
||||
bool isCalendarOperation = requestList.All(request => request is ICalendarActionRequest);
|
||||
|
||||
if (isCalendarOperation)
|
||||
{
|
||||
|
||||
@@ -93,13 +93,11 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
||||
// Queue requests for each account and start synchronization.
|
||||
foreach (var accountGroup in accountIds)
|
||||
{
|
||||
foreach (var accountRequest in accountGroup)
|
||||
{
|
||||
await QueueRequestAsync(accountRequest, accountGroup.Key);
|
||||
}
|
||||
var groupedRequests = accountGroup.Cast<IRequestBase>().ToList();
|
||||
await QueueRequestsAsync(groupedRequests, accountGroup.Key).ConfigureAwait(false);
|
||||
|
||||
var account = accountGroup.First().Item.AssignedAccount;
|
||||
var actionItems = SynchronizationActionHelper.CreateActionItems(accountGroup, accountGroup.Key, account.Name);
|
||||
var actionItems = SynchronizationActionHelper.CreateActionItems(groupedRequests, accountGroup.Key, account.Name);
|
||||
|
||||
if (actionItems.Count > 0)
|
||||
WeakReferenceMessenger.Default.Send(new SynchronizationActionsAdded(accountGroup.Key, account.Name, actionItems));
|
||||
@@ -214,10 +212,7 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
||||
if (requestList.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var request in requestList)
|
||||
{
|
||||
await QueueRequestAsync(request, accountId).ConfigureAwait(false);
|
||||
}
|
||||
await QueueRequestsAsync(requestList, accountId).ConfigureAwait(false);
|
||||
|
||||
await SendSyncActionsAddedAsync(requestList, accountId).ConfigureAwait(false);
|
||||
await QueueSynchronizationAsync(accountId).ConfigureAwait(false);
|
||||
@@ -274,7 +269,12 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
||||
private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
|
||||
{
|
||||
// Don't trigger synchronization for individual requests - we'll trigger it once for all requests
|
||||
await SynchronizationManager.Instance.QueueRequestAsync(request, accountId, triggerSynchronization: false);
|
||||
await SynchronizationManager.Instance.QueueRequestAsync(request, accountId, triggerSynchronization: false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task QueueRequestsAsync(IEnumerable<IRequestBase> requests, Guid accountId)
|
||||
{
|
||||
await SynchronizationManager.Instance.QueueRequestsAsync(requests, accountId, triggerSynchronization: false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task QueueSynchronizationAsync(Guid accountId)
|
||||
|
||||
@@ -54,6 +54,10 @@ public class WinoRequestProcessor : IWinoRequestProcessor
|
||||
{
|
||||
var action = preperationRequest.Action;
|
||||
var moveTargetStructure = preperationRequest.MoveTargetFolder;
|
||||
var mailItems = preperationRequest.MailItems?.Where(item => item != null).ToList() ?? [];
|
||||
|
||||
if (mailItems.Count == 0)
|
||||
return [];
|
||||
|
||||
// Ask confirmation for permanent delete operation.
|
||||
// Drafts are always hard deleted without any protection.
|
||||
@@ -78,12 +82,12 @@ public class WinoRequestProcessor : IWinoRequestProcessor
|
||||
// Handle the case when user is trying to move multiple mails that belong to different accounts.
|
||||
// We can't handle this with only 1 picker dialog.
|
||||
|
||||
bool isInvalidMoveTarget = preperationRequest.MailItems.Select(a => a.AssignedAccount.Id).Distinct().Count() > 1;
|
||||
bool isInvalidMoveTarget = mailItems.Select(a => a.AssignedAccount.Id).Distinct().Count() > 1;
|
||||
|
||||
if (isInvalidMoveTarget)
|
||||
throw new InvalidMoveTargetException(InvalidMoveTargetReason.MultipleAccounts);
|
||||
|
||||
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
|
||||
var accountId = mailItems[0].AssignedAccount.Id;
|
||||
|
||||
moveTargetStructure = await _dialogService.PickFolderAsync(accountId, PickFolderReason.Move, _folderService);
|
||||
|
||||
@@ -94,7 +98,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor
|
||||
var requests = new List<IMailActionRequest>();
|
||||
|
||||
// TODO: Fix: Collection was modified; enumeration operation may not execute
|
||||
foreach (var item in preperationRequest.MailItems.ToList())
|
||||
foreach (var item in mailItems)
|
||||
{
|
||||
var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
@@ -262,4 +263,75 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected void ApplyOptimisticUiChanges(IEnumerable<IRequestBundle<TBaseRequest>> bundles, Func<IRequestBase, bool> shouldApply = null)
|
||||
{
|
||||
var bundleList = bundles?
|
||||
.Where(b => b?.Request != null && (shouldApply?.Invoke(b.Request) ?? true))
|
||||
.ToList() ?? [];
|
||||
|
||||
if (bundleList.Count == 0)
|
||||
return;
|
||||
|
||||
var requestList = new List<IRequestBase>(bundleList.Count);
|
||||
|
||||
foreach (var bundle in bundleList)
|
||||
{
|
||||
if (bundle.UIChangeRequest != null && !ReferenceEquals(bundle.UIChangeRequest, bundle.Request))
|
||||
{
|
||||
bundle.UIChangeRequest.ApplyUIChanges();
|
||||
continue;
|
||||
}
|
||||
|
||||
requestList.Add(bundle.Request);
|
||||
}
|
||||
|
||||
if (requestList.Count == 0)
|
||||
return;
|
||||
|
||||
var appliedBatchRequestKeys = new HashSet<object>();
|
||||
|
||||
foreach (var group in requestList.GroupBy(r => r.GroupingKey()))
|
||||
{
|
||||
var groupRequests = group.ToList();
|
||||
if (groupRequests.Count <= 1)
|
||||
continue;
|
||||
|
||||
if (!TryApplyBatchUiChanges(groupRequests))
|
||||
continue;
|
||||
|
||||
appliedBatchRequestKeys.Add(group.Key);
|
||||
}
|
||||
|
||||
foreach (var request in requestList)
|
||||
{
|
||||
if (!appliedBatchRequestKeys.Contains(request.GroupingKey()))
|
||||
{
|
||||
request.ApplyUIChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyBatchUiChanges(IReadOnlyList<IRequestBase> requests)
|
||||
{
|
||||
if (requests == null || requests.Count <= 1)
|
||||
return false;
|
||||
|
||||
return requests[0] switch
|
||||
{
|
||||
MarkReadRequest => ApplyBatch(new BatchMarkReadRequest(requests.Cast<MarkReadRequest>())),
|
||||
ChangeFlagRequest => ApplyBatch(new BatchChangeFlagRequest(requests.Cast<ChangeFlagRequest>())),
|
||||
DeleteRequest => ApplyBatch(new BatchDeleteRequest(requests.Cast<DeleteRequest>())),
|
||||
MoveRequest => ApplyBatch(new BatchMoveRequest(requests.Cast<MoveRequest>())),
|
||||
ArchiveRequest => ApplyBatch(new BatchArchiveRequest(requests.Cast<ArchiveRequest>())),
|
||||
ChangeJunkStateRequest => ApplyBatch(new BatchChangeJunkStateRequest(requests.Cast<ChangeJunkStateRequest>())),
|
||||
_ => false
|
||||
};
|
||||
|
||||
static bool ApplyBatch(IUIChangeRequest request)
|
||||
{
|
||||
request.ApplyUIChanges();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Wino.Core.Synchronizers.ImapSync;
|
||||
public class UnifiedImapSynchronizer
|
||||
{
|
||||
private static readonly TimeSpan UidReconcileInterval = TimeSpan.FromHours(12);
|
||||
private const int NewMessageFetchBatchSize = 50;
|
||||
private const int ExistingMessageFlagFetchBatchSize = 250;
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<UnifiedImapSynchronizer>();
|
||||
private readonly IFolderService _folderService;
|
||||
@@ -47,6 +49,9 @@ public class UnifiedImapSynchronizer
|
||||
MessageSummaryItems.References |
|
||||
MessageSummaryItems.ModSeq |
|
||||
MessageSummaryItems.BodyStructure;
|
||||
private readonly MessageSummaryItems _existingMailSynchronizationFlags =
|
||||
MessageSummaryItems.Flags |
|
||||
MessageSummaryItems.UniqueId;
|
||||
|
||||
public UnifiedImapSynchronizer(
|
||||
IFolderService folderService,
|
||||
@@ -182,15 +187,35 @@ public class UnifiedImapSynchronizer
|
||||
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
foreach (var batch in uids.Distinct().OrderBy(a => a.Id).Batch(50))
|
||||
foreach (var batch in uids.Distinct().OrderBy(a => a.Id).Batch(ExistingMessageFlagFetchBatchSize))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var summaryBatch = await remoteFolder
|
||||
.FetchAsync(new UniqueIdSet(batch.ToList(), SortOrder.Ascending), _mailSynchronizationFlags, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var batchUids = batch.ToList();
|
||||
var existingMails = await _mailService.GetExistingMailsAsync(localFolder.Id, batchUids).ConfigureAwait(false);
|
||||
var existingByUid = CreateExistingMailLookup(existingMails);
|
||||
var existingUids = batchUids.Where(uid => existingByUid.ContainsKey(uid.Id)).ToList();
|
||||
var newUids = batchUids.Where(uid => !existingByUid.ContainsKey(uid.Id)).ToList();
|
||||
|
||||
downloadedMessageIds.AddRange(await ProcessSummariesAsync(synchronizer, localFolder, summaryBatch, cancellationToken).ConfigureAwait(false));
|
||||
if (existingUids.Count > 0)
|
||||
{
|
||||
var existingSummaryBatch = await remoteFolder
|
||||
.FetchAsync(new UniqueIdSet(existingUids, SortOrder.Ascending), _existingMailSynchronizationFlags, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await ApplySummaryFlagUpdatesAsync(existingByUid, existingSummaryBatch).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var newBatch in newUids.Batch(NewMessageFetchBatchSize))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var newSummaryBatch = await remoteFolder
|
||||
.FetchAsync(new UniqueIdSet(newBatch.ToList(), SortOrder.Ascending), _mailSynchronizationFlags, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
downloadedMessageIds.AddRange(await ProcessSummariesCoreAsync(synchronizer, localFolder, newSummaryBatch, existingByUid, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
|
||||
UpdateHighestKnownUid(localFolder, remoteFolder, uids.Select(a => a.Id));
|
||||
@@ -268,7 +293,29 @@ public class UnifiedImapSynchronizer
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
var existingMails = await _mailService.GetExistingMailsAsync(folder.Id, changedUids).ConfigureAwait(false);
|
||||
var existingByUid = CreateExistingMailLookup(existingMails);
|
||||
var newOrUnknownUids = changedUids.Where(uid => !existingByUid.ContainsKey(uid.Id)).ToList();
|
||||
var existingUidsWithoutFlagEvents = changedUids
|
||||
.Where(uid => existingByUid.ContainsKey(uid.Id) && !changedFlags.ContainsKey(uid.Id))
|
||||
.ToList();
|
||||
|
||||
if (existingUidsWithoutFlagEvents.Count > 0)
|
||||
{
|
||||
var missingEventSummaries = await remoteFolder
|
||||
.FetchAsync(new UniqueIdSet(existingUidsWithoutFlagEvents, SortOrder.Ascending), _existingMailSynchronizationFlags, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var summary in missingEventSummaries)
|
||||
{
|
||||
if (summary.UniqueId != UniqueId.Invalid && summary.Flags != null)
|
||||
{
|
||||
changedFlags[summary.UniqueId.Id] = summary.Flags.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, newOrUnknownUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
|
||||
|
||||
@@ -456,11 +503,19 @@ public class UnifiedImapSynchronizer
|
||||
folder.UidValidity = remoteFolder.UidValidity;
|
||||
}
|
||||
|
||||
private async Task<List<string>> ProcessSummariesAsync(
|
||||
private Task<List<string>> ProcessSummariesAsync(
|
||||
IImapSynchronizer synchronizer,
|
||||
MailItemFolder localFolder,
|
||||
IList<IMessageSummary> summaries,
|
||||
CancellationToken cancellationToken)
|
||||
=> ProcessSummariesCoreAsync(synchronizer, localFolder, summaries, existingByUid: null, cancellationToken);
|
||||
|
||||
private async Task<List<string>> ProcessSummariesCoreAsync(
|
||||
IImapSynchronizer synchronizer,
|
||||
MailItemFolder localFolder,
|
||||
IList<IMessageSummary> summaries,
|
||||
IReadOnlyDictionary<uint, MailCopy> existingByUid,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
@@ -475,10 +530,8 @@ public class UnifiedImapSynchronizer
|
||||
if (uniqueIds.Count == 0)
|
||||
return downloadedMessageIds;
|
||||
|
||||
var existingMails = await _mailService.GetExistingMailsAsync(localFolder.Id, uniqueIds).ConfigureAwait(false);
|
||||
var existingByUid = existingMails
|
||||
.Select(m => (Uid: MailkitClientExtensions.ResolveUidStruct(m.Id), Mail: m))
|
||||
.ToDictionary(a => a.Uid.Id, a => a.Mail);
|
||||
existingByUid ??= CreateExistingMailLookup(await _mailService.GetExistingMailsAsync(localFolder.Id, uniqueIds).ConfigureAwait(false));
|
||||
var pendingStateUpdates = new List<MailCopyStateUpdate>();
|
||||
|
||||
foreach (var summary in summaries)
|
||||
{
|
||||
@@ -491,7 +544,11 @@ public class UnifiedImapSynchronizer
|
||||
{
|
||||
if (summary.Flags != null)
|
||||
{
|
||||
await UpdateMailFlagsAsync(existingMail, summary.Flags.Value).ConfigureAwait(false);
|
||||
var pendingStateUpdate = CreateMailStateUpdate(existingMail, summary.Flags.Value);
|
||||
if (pendingStateUpdate != null)
|
||||
{
|
||||
pendingStateUpdates.Add(pendingStateUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -516,23 +573,79 @@ public class UnifiedImapSynchronizer
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingStateUpdates.Count > 0)
|
||||
{
|
||||
await _mailService.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
private async Task UpdateMailFlagsAsync(MailCopy mailCopy, MessageFlags flags)
|
||||
private async Task ApplySummaryFlagUpdatesAsync(
|
||||
IReadOnlyDictionary<uint, MailCopy> existingByUid,
|
||||
IList<IMessageSummary> summaries)
|
||||
{
|
||||
if (existingByUid == null || existingByUid.Count == 0 || summaries == null || summaries.Count == 0)
|
||||
return;
|
||||
|
||||
var pendingStateUpdates = new List<MailCopyStateUpdate>();
|
||||
|
||||
foreach (var summary in summaries)
|
||||
{
|
||||
if (summary.UniqueId == UniqueId.Invalid || summary.Flags == null)
|
||||
continue;
|
||||
|
||||
if (!existingByUid.TryGetValue(summary.UniqueId.Id, out var existingMail))
|
||||
continue;
|
||||
|
||||
var pendingStateUpdate = CreateMailStateUpdate(existingMail, summary.Flags.Value);
|
||||
if (pendingStateUpdate != null)
|
||||
{
|
||||
pendingStateUpdates.Add(pendingStateUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingStateUpdates.Count > 0)
|
||||
{
|
||||
await _mailService.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<uint, MailCopy> CreateExistingMailLookup(IEnumerable<MailCopy> existingMails)
|
||||
{
|
||||
var lookup = new Dictionary<uint, MailCopy>();
|
||||
|
||||
foreach (var mail in existingMails ?? [])
|
||||
{
|
||||
if (mail == null || string.IsNullOrEmpty(mail.Id))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
lookup[MailkitClientExtensions.ResolveUidStruct(mail.Id).Id] = mail;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private static MailCopyStateUpdate CreateMailStateUpdate(MailCopy mailCopy, MessageFlags flags)
|
||||
{
|
||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
|
||||
var isRead = MailkitClientExtensions.GetIsRead(flags);
|
||||
|
||||
if (isFlagged != mailCopy.IsFlagged)
|
||||
{
|
||||
await _mailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false);
|
||||
}
|
||||
bool shouldUpdateFlagged = isFlagged != mailCopy.IsFlagged;
|
||||
bool shouldUpdateRead = isRead != mailCopy.IsRead;
|
||||
|
||||
if (isRead != mailCopy.IsRead)
|
||||
{
|
||||
await _mailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false);
|
||||
}
|
||||
return !shouldUpdateFlagged && !shouldUpdateRead
|
||||
? null
|
||||
: new MailCopyStateUpdate(
|
||||
mailCopy.Id,
|
||||
shouldUpdateRead ? isRead : null,
|
||||
shouldUpdateFlagged ? isFlagged : null);
|
||||
}
|
||||
|
||||
private async Task ApplyDeletedUidsAsync(MailItemFolder folder, IList<UniqueId> uniqueIds)
|
||||
@@ -552,15 +665,14 @@ public class UnifiedImapSynchronizer
|
||||
if (changedFlags == null || changedFlags.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var changed in changedFlags)
|
||||
{
|
||||
var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, changed.Key);
|
||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(changed.Value);
|
||||
var isRead = MailkitClientExtensions.GetIsRead(changed.Value);
|
||||
var stateUpdates = changedFlags
|
||||
.Select(changed => new MailCopyStateUpdate(
|
||||
MailkitClientExtensions.CreateUid(folder.Id, changed.Key),
|
||||
MailkitClientExtensions.GetIsRead(changed.Value),
|
||||
MailkitClientExtensions.GetIsFlagged(changed.Value)))
|
||||
.ToList();
|
||||
|
||||
await _mailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false);
|
||||
await _mailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false);
|
||||
}
|
||||
await _mailService.ApplyMailStateUpdatesAsync(stateUpdates).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ReconcileUidBasedFlagChangesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken)
|
||||
@@ -613,13 +725,14 @@ public class UnifiedImapSynchronizer
|
||||
|
||||
var existingMarkReadCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, markReadCandidates, cancellationToken).ConfigureAwait(false);
|
||||
var existingUnflagCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, unflagCandidates, cancellationToken).ConfigureAwait(false);
|
||||
var pendingStateUpdates = new List<MailCopyStateUpdate>();
|
||||
|
||||
foreach (var uid in existingMarkReadCandidates)
|
||||
{
|
||||
if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsRead)
|
||||
continue;
|
||||
|
||||
await _mailService.ChangeReadStatusAsync(localMail.Id, true).ConfigureAwait(false);
|
||||
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsRead: true));
|
||||
}
|
||||
|
||||
foreach (var uid in remoteUnreadUids)
|
||||
@@ -627,7 +740,7 @@ public class UnifiedImapSynchronizer
|
||||
if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsRead)
|
||||
continue;
|
||||
|
||||
await _mailService.ChangeReadStatusAsync(localMail.Id, false).ConfigureAwait(false);
|
||||
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsRead: false));
|
||||
}
|
||||
|
||||
foreach (var uid in existingUnflagCandidates)
|
||||
@@ -635,7 +748,7 @@ public class UnifiedImapSynchronizer
|
||||
if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsFlagged)
|
||||
continue;
|
||||
|
||||
await _mailService.ChangeFlagStatusAsync(localMail.Id, false).ConfigureAwait(false);
|
||||
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsFlagged: false));
|
||||
}
|
||||
|
||||
foreach (var uid in remoteFlaggedUids)
|
||||
@@ -643,7 +756,12 @@ public class UnifiedImapSynchronizer
|
||||
if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsFlagged)
|
||||
continue;
|
||||
|
||||
await _mailService.ChangeFlagStatusAsync(localMail.Id, true).ConfigureAwait(false);
|
||||
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsFlagged: true));
|
||||
}
|
||||
|
||||
if (pendingStateUpdates.Count > 0)
|
||||
{
|
||||
await _mailService.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,56 +112,104 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
|
||||
public override List<IRequestBundle<ImapRequest>> Move(BatchMoveRequest requests)
|
||||
{
|
||||
return CreateTaskBundle(async (client, item) =>
|
||||
{
|
||||
var sourceFolder = await client.GetFolderAsync(item.FromFolder.RemoteFolderId);
|
||||
var destinationFolder = await client.GetFolderAsync(item.ToFolder.RemoteFolderId);
|
||||
if (requests == null || requests.Count == 0)
|
||||
return [];
|
||||
|
||||
return CreateSingleTaskBundle(async (client, _) =>
|
||||
{
|
||||
var sourceFolder = await client.GetFolderAsync(requests[0].FromFolder.RemoteFolderId).ConfigureAwait(false);
|
||||
var destinationFolder = await client.GetFolderAsync(requests[0].ToFolder.RemoteFolderId).ConfigureAwait(false);
|
||||
var uniqueIds = requests.Select(item => GetUniqueId(item.Item.Id)).ToList();
|
||||
|
||||
// Only opening source folder is enough.
|
||||
await sourceFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
|
||||
await sourceFolder.MoveToAsync(GetUniqueId(item.Item.Id), destinationFolder).ConfigureAwait(false);
|
||||
await sourceFolder.CloseAsync().ConfigureAwait(false);
|
||||
}, requests);
|
||||
try
|
||||
{
|
||||
await sourceFolder.MoveToAsync(uniqueIds, destinationFolder).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await sourceFolder.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
}, requests[0], requests);
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<ImapRequest>> ChangeFlag(BatchChangeFlagRequest requests)
|
||||
{
|
||||
return CreateTaskBundle(async (client, item) =>
|
||||
if (requests == null || requests.Count == 0)
|
||||
return [];
|
||||
|
||||
return CreateSingleTaskBundle(async (client, _) =>
|
||||
{
|
||||
var folder = item.Item.AssignedFolder;
|
||||
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId);
|
||||
var folder = requests[0].Item.AssignedFolder;
|
||||
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
|
||||
var uniqueIds = requests.Select(item => GetUniqueId(item.Item.Id)).ToList();
|
||||
var request = new StoreFlagsRequest(requests[0].IsFlagged ? StoreAction.Add : StoreAction.Remove, MessageFlags.Flagged)
|
||||
{
|
||||
Silent = true
|
||||
};
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
|
||||
await remoteFolder.StoreAsync(GetUniqueId(item.Item.Id), new StoreFlagsRequest(item.IsFlagged ? StoreAction.Add : StoreAction.Remove, MessageFlags.Flagged) { Silent = true }).ConfigureAwait(false);
|
||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||
}, requests);
|
||||
try
|
||||
{
|
||||
await remoteFolder.StoreAsync(uniqueIds, request).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
}, requests[0], requests);
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<ImapRequest>> Delete(BatchDeleteRequest requests)
|
||||
{
|
||||
return CreateTaskBundle(async (client, request) =>
|
||||
if (requests == null || requests.Count == 0)
|
||||
return [];
|
||||
|
||||
return CreateSingleTaskBundle(async (client, _) =>
|
||||
{
|
||||
var folder = request.Item.AssignedFolder;
|
||||
var folder = requests[0].Item.AssignedFolder;
|
||||
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
|
||||
var uniqueIds = requests.Select(request => GetUniqueId(request.Item.Id)).ToList();
|
||||
var storeRequest = new StoreFlagsRequest(StoreAction.Add, MessageFlags.Deleted) { Silent = true };
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
|
||||
await remoteFolder.AddFlagsAsync(GetUniqueId(request.Item.Id), MessageFlags.Deleted, true);
|
||||
await remoteFolder.ExpungeAsync().ConfigureAwait(false);
|
||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||
}, requests);
|
||||
try
|
||||
{
|
||||
await remoteFolder.StoreAsync(uniqueIds, storeRequest).ConfigureAwait(false);
|
||||
await remoteFolder.ExpungeAsync(uniqueIds).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
}, requests[0], requests);
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<ImapRequest>> MarkRead(BatchMarkReadRequest requests)
|
||||
{
|
||||
return CreateTaskBundle(async (client, request) =>
|
||||
if (requests == null || requests.Count == 0)
|
||||
return [];
|
||||
|
||||
return CreateSingleTaskBundle(async (client, _) =>
|
||||
{
|
||||
var folder = request.Item.AssignedFolder;
|
||||
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId);
|
||||
var folder = requests[0].Item.AssignedFolder;
|
||||
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
|
||||
var uniqueIds = requests.Select(request => GetUniqueId(request.Item.Id)).ToList();
|
||||
var storeRequest = new StoreFlagsRequest(requests[0].IsRead ? StoreAction.Add : StoreAction.Remove, MessageFlags.Seen)
|
||||
{
|
||||
Silent = true
|
||||
};
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
|
||||
await remoteFolder.StoreAsync(GetUniqueId(request.Item.Id), new StoreFlagsRequest(request.IsRead ? StoreAction.Add : StoreAction.Remove, MessageFlags.Seen) { Silent = true }).ConfigureAwait(false);
|
||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||
}, requests);
|
||||
try
|
||||
{
|
||||
await remoteFolder.StoreAsync(uniqueIds, storeRequest).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
}, requests[0], requests);
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<ImapRequest>> CreateDraft(CreateDraftRequest request)
|
||||
@@ -718,13 +766,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
// First apply the UI changes for each bundle.
|
||||
// This is important to reflect changes to the UI before the network call is done.
|
||||
|
||||
foreach (var item in batchedRequests)
|
||||
{
|
||||
if (ShouldApplyOptimisticUIChanges(item.Request))
|
||||
{
|
||||
item.Request.ApplyUIChanges();
|
||||
}
|
||||
}
|
||||
ApplyOptimisticUiChanges(batchedRequests, ShouldApplyOptimisticUIChanges);
|
||||
|
||||
// All task bundles will execute on the same client.
|
||||
// Tasks themselves don't pull the client from the pool
|
||||
@@ -754,7 +796,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
|
||||
if (ShouldApplyOptimisticUIChanges(item.Request))
|
||||
{
|
||||
item.Request.RevertUIChanges();
|
||||
item.UIChangeRequest?.RevertUIChanges();
|
||||
}
|
||||
|
||||
isCrashed = true;
|
||||
@@ -795,7 +837,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
|
||||
if (ShouldApplyOptimisticUIChanges(item.Request))
|
||||
{
|
||||
item.Request.RevertUIChanges();
|
||||
item.UIChangeRequest?.RevertUIChanges();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -2020,10 +2020,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
{
|
||||
// First apply all UI changes immediately before any batching.
|
||||
// This ensures UI reflects changes right away, regardless of batch processing.
|
||||
foreach (var bundle in batchedRequests)
|
||||
{
|
||||
bundle.UIChangeRequest?.ApplyUIChanges();
|
||||
}
|
||||
ApplyOptimisticUiChanges(batchedRequests);
|
||||
|
||||
// SendDraft requests may include large attachments, which require upload sessions.
|
||||
// Upload these attachments before the batched patch/send sequence.
|
||||
|
||||
@@ -161,11 +161,11 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
|
||||
foreach (var group in keys)
|
||||
{
|
||||
var key = group.Key;
|
||||
var firstRequest = group.FirstOrDefault();
|
||||
|
||||
if (key is MailSynchronizerOperation mailSynchronizerOperation)
|
||||
if (firstRequest is IMailActionRequest mailActionRequest)
|
||||
{
|
||||
switch (mailSynchronizerOperation)
|
||||
switch (mailActionRequest.Operation)
|
||||
{
|
||||
case MailSynchronizerOperation.MarkRead:
|
||||
nativeRequests.AddRange(MarkRead(new BatchMarkReadRequest(group.Cast<MarkReadRequest>())));
|
||||
@@ -204,9 +204,9 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (key is FolderSynchronizerOperation folderSynchronizerOperation)
|
||||
else if (firstRequest is IFolderActionRequest folderActionRequest)
|
||||
{
|
||||
switch (folderSynchronizerOperation)
|
||||
switch (folderActionRequest.Operation)
|
||||
{
|
||||
case FolderSynchronizerOperation.RenameFolder:
|
||||
nativeRequests.AddRange(RenameFolder(group.ElementAt(0) as RenameFolderRequest));
|
||||
@@ -230,9 +230,9 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (key is CategorySynchronizerOperation categorySynchronizerOperation)
|
||||
else if (firstRequest is ICategoryActionRequest categoryActionRequest)
|
||||
{
|
||||
switch (categorySynchronizerOperation)
|
||||
switch (categoryActionRequest.Operation)
|
||||
{
|
||||
case CategorySynchronizerOperation.CreateCategory:
|
||||
nativeRequests.AddRange(CreateCategory(group.ElementAt(0) as MailCategoryCreateRequest));
|
||||
|
||||
Reference in New Issue
Block a user