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
+205 -18
View File
@@ -517,6 +517,53 @@ public class MailService : BaseDatabaseService, IMailService
}
}
private async Task PopulateAssignedPropertiesAsync(List<MailCopy> mails)
{
if (mails == null || mails.Count == 0)
return;
var folderIds = mails
.Select(m => m.FolderId)
.Distinct()
.ToList();
if (folderIds.Count == 0)
return;
var folders = await Task.WhenAll(folderIds.Select(id => _folderService.GetFolderAsync(id))).ConfigureAwait(false);
var folderCache = folders
.Where(f => f != null)
.ToDictionary(f => f.Id);
if (folderCache.Count == 0)
return;
var accountIds = folderCache.Values
.Select(f => f.MailAccountId)
.Distinct()
.ToHashSet();
var allAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
var accountCache = allAccounts
.Where(a => accountIds.Contains(a.Id))
.ToDictionary(a => a.Id);
var addresses = mails
.Where(m => !string.IsNullOrEmpty(m.FromAddress))
.Select(m => m.FromAddress)
.Distinct()
.ToList();
var contactCache = addresses.Count == 0
? new Dictionary<string, AccountContact>()
: (await _contactService.GetContactsByAddressesAsync(addresses).ConfigureAwait(false))
.Where(c => c != null)
.ToDictionary(c => c.Address);
AssignPropertiesFromCaches(mails, folderCache, accountCache, contactCache);
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
}
private async Task<List<MailCopy>> GetMailsByThreadIdsAsync(List<string> threadIds, HashSet<string> excludeMailIds)
{
if (threadIds?.Count == 0)
@@ -565,11 +612,34 @@ public class MailService : BaseDatabaseService, IMailService
private async Task<List<MailCopy>> GetMailItemsAsync(string mailCopyId)
{
var mailCopies = await Connection.Table<MailCopy>().Where(a => a.Id == mailCopyId).ToListAsync();
var mailCopies = await GetMailCopiesByIdAsync([mailCopyId]).ConfigureAwait(false);
await PopulateAssignedPropertiesAsync(mailCopies).ConfigureAwait(false);
foreach (var mailCopy in mailCopies)
return mailCopies;
}
private async Task<List<MailCopy>> GetMailCopiesByIdAsync(IEnumerable<string> mailCopyIds)
{
var distinctMailCopyIds = mailCopyIds?
.Where(a => !string.IsNullOrWhiteSpace(a))
.Distinct(StringComparer.Ordinal)
.ToList();
if (distinctMailCopyIds == null || distinctMailCopyIds.Count == 0)
return [];
var mailCopies = new List<MailCopy>();
const int batchSize = 200;
for (int i = 0; i < distinctMailCopyIds.Count; i += batchSize)
{
await LoadAssignedPropertiesAsync(mailCopy).ConfigureAwait(false);
var batchIds = distinctMailCopyIds.Skip(i).Take(batchSize).ToList();
var placeholders = string.Join(",", batchIds.Select(_ => "?"));
var sql = $"SELECT * FROM MailCopy WHERE Id IN ({placeholders})";
var batch = await Connection.QueryAsync<MailCopy>(sql, batchIds.Cast<object>().ToArray()).ConfigureAwait(false);
mailCopies.AddRange(batch);
}
return mailCopies;
@@ -754,9 +824,51 @@ public class MailService : BaseDatabaseService, IMailService
#endregion
private async Task UpdateAllMailCopiesAsync(string mailCopyId, Func<MailCopy, bool> action)
private async Task PersistMailCopyUpdatesAsync(IReadOnlyList<(MailCopy MailCopy, MailCopyChangeFlags ChangedProperties)> pendingUpdates)
{
var mailCopies = await GetMailItemsAsync(mailCopyId);
if (pendingUpdates == null || pendingUpdates.Count == 0)
return;
await Connection.RunInTransactionAsync(connection =>
{
foreach (var (mailCopy, _) in pendingUpdates)
{
connection.Update(mailCopy, typeof(MailCopy));
}
}).ConfigureAwait(false);
var readMailUniqueIds = pendingUpdates
.Where(x => (x.ChangedProperties & MailCopyChangeFlags.IsRead) != 0 &&
x.MailCopy?.IsRead == true &&
x.MailCopy.UniqueId != Guid.Empty)
.Select(x => x.MailCopy.UniqueId)
.Distinct()
.ToList();
if (readMailUniqueIds.Count > 0)
{
WeakReferenceMessenger.Default.Send(new BulkMailReadStatusChanged(readMailUniqueIds));
}
foreach (var updateGroup in pendingUpdates
.Where(x => x.MailCopy != null)
.GroupBy(x => x.ChangedProperties))
{
var updatedMails = updateGroup
.Select(x => x.MailCopy)
.Where(x => x != null)
.ToList();
if (updatedMails.Count == 0)
continue;
ReportUIChange(new BulkMailUpdatedMessage(updatedMails, EntityUpdateSource.Server, updateGroup.Key));
}
}
private async Task UpdateAllMailCopiesAsync(string mailCopyId, Func<MailCopy, MailCopyChangeFlags> action)
{
var mailCopies = await GetMailCopiesByIdAsync([mailCopyId]).ConfigureAwait(false);
if (mailCopies == null || !mailCopies.Any())
{
@@ -767,43 +879,110 @@ public class MailService : BaseDatabaseService, IMailService
_logger.Debug("Updating {MailCopyCount} mail copies with Id {MailCopyId}", mailCopies.Count, mailCopyId);
var pendingUpdates = new List<(MailCopy MailCopy, MailCopyChangeFlags ChangedProperties)>();
foreach (var mailCopy in mailCopies)
{
bool shouldUpdateItem = action(mailCopy);
var changedProperties = action(mailCopy);
if (shouldUpdateItem)
if (changedProperties != MailCopyChangeFlags.None)
{
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
pendingUpdates.Add((mailCopy, changedProperties));
}
else
{
_logger.Debug("Skipped updating mail because it is already in the desired state.");
}
}
await PersistMailCopyUpdatesAsync(pendingUpdates).ConfigureAwait(false);
}
public Task ChangeReadStatusAsync(string mailCopyId, bool isRead)
=> UpdateAllMailCopiesAsync(mailCopyId, (item) =>
{
if (item.IsRead == isRead) return false;
if (item.IsRead == isRead) return MailCopyChangeFlags.None;
item.IsRead = isRead;
if (isRead && item.UniqueId != Guid.Empty)
{
WeakReferenceMessenger.Default.Send(new MailReadStatusChanged(item.UniqueId));
}
return true;
return MailCopyChangeFlags.IsRead;
});
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
=> UpdateAllMailCopiesAsync(mailCopyId, (item) =>
{
if (item.IsFlagged == isFlagged) return false;
if (item.IsFlagged == isFlagged) return MailCopyChangeFlags.None;
item.IsFlagged = isFlagged;
return true;
return MailCopyChangeFlags.IsFlagged;
});
public async Task ApplyMailStateUpdatesAsync(IEnumerable<MailCopyStateUpdate> updates)
{
var updateLookup = new Dictionary<string, MailCopyStateUpdate>(StringComparer.Ordinal);
foreach (var update in updates ?? [])
{
if (update == null || string.IsNullOrWhiteSpace(update.MailCopyId))
continue;
if (updateLookup.TryGetValue(update.MailCopyId, out var existingUpdate))
{
updateLookup[update.MailCopyId] = new MailCopyStateUpdate(
update.MailCopyId,
update.IsRead ?? existingUpdate.IsRead,
update.IsFlagged ?? existingUpdate.IsFlagged);
}
else
{
updateLookup[update.MailCopyId] = update;
}
}
if (updateLookup.Count == 0)
return;
var mailCopies = await GetMailCopiesByIdAsync(updateLookup.Keys).ConfigureAwait(false);
if (mailCopies.Count == 0)
{
_logger.Warning("Applying mail state updates failed because there are no matching copies for {MailCopyCount} ids.", updateLookup.Count);
return;
}
await PopulateAssignedPropertiesAsync(mailCopies).ConfigureAwait(false);
var pendingUpdates = new List<(MailCopy MailCopy, MailCopyChangeFlags ChangedProperties)>();
foreach (var mailCopy in mailCopies)
{
if (!updateLookup.TryGetValue(mailCopy.Id, out var update))
continue;
var changedProperties = MailCopyChangeFlags.None;
if (update.IsRead.HasValue && mailCopy.IsRead != update.IsRead.Value)
{
mailCopy.IsRead = update.IsRead.Value;
changedProperties |= MailCopyChangeFlags.IsRead;
}
if (update.IsFlagged.HasValue && mailCopy.IsFlagged != update.IsFlagged.Value)
{
mailCopy.IsFlagged = update.IsFlagged.Value;
changedProperties |= MailCopyChangeFlags.IsFlagged;
}
if (changedProperties != MailCopyChangeFlags.None)
{
pendingUpdates.Add((mailCopy, changedProperties));
}
}
await PersistMailCopyUpdatesAsync(pendingUpdates).ConfigureAwait(false);
}
public async Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
{
// Note: Folder might not be available at the moment due to user not syncing folders before the delta processing.
@@ -1396,18 +1575,26 @@ public class MailService : BaseDatabaseService, IMailService
(shouldUpdateDraftId && item.DraftId != newDraftId))
{
var oldDraftId = item.DraftId;
var changedProperties = MailCopyChangeFlags.None;
if (shouldUpdateDraftId)
{
item.DraftId = newDraftId;
changedProperties |= MailCopyChangeFlags.DraftId;
}
if (shouldUpdateThreadId)
{
item.ThreadId = newThreadId;
changedProperties |= MailCopyChangeFlags.ThreadId;
}
ReportUIChange(new DraftMapped(oldDraftId, item.DraftId));
return true;
return changedProperties;
}
return false;
return MailCopyChangeFlags.None;
});
}