Fixing UI thread issues with bulk operations and request queue refactoring.

This commit is contained in:
Burak Kaan Köse
2026-04-20 02:18:23 +02:00
parent 3bd0b69429
commit 54148716bb
38 changed files with 1644 additions and 206 deletions
@@ -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);
}
}
+77 -35
View File
@@ -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.
+7 -7
View File
@@ -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));