Add local mail pinning support

This commit is contained in:
Burak Kaan Köse
2026-04-21 23:17:08 +02:00
parent c0023614ad
commit 09820dda71
19 changed files with 531 additions and 53 deletions
@@ -259,6 +259,26 @@ public class MailFetchingTests : IAsyncLifetime
result.Single().FolderId.Should().Be(_inboxFolder.Id, "a copy from the actively searched folder should win over newer non-searched copies");
}
[Fact]
public async Task FetchPinnedMailsAsync_ReturnsPinnedMailsOutsideRegularPage()
{
var oldPinned = BuildMail(_inboxFolder.Id, DateTime.UtcNow.AddDays(-5));
oldPinned.IsPinned = true;
var recentMails = Enumerable.Range(0, 120)
.Select(i => BuildMail(_inboxFolder.Id, DateTime.UtcNow.AddMinutes(-i)))
.ToList();
await _databaseService.Connection.InsertAsync(oldPinned, typeof(MailCopy));
await _databaseService.Connection.InsertAllAsync(recentMails, typeof(MailCopy));
var options = BuildOptions([_inboxFolder], createThreads: false, take: 20);
var result = await _mailService.FetchPinnedMailsAsync(options);
result.Should().ContainSingle(mail => mail.UniqueId == oldPinned.UniqueId);
}
[Fact]
public async Task CreateAssignmentAsync_ExistingAssignment_IsIgnored()
{
@@ -297,6 +317,27 @@ public class MailFetchingTests : IAsyncLifetime
insertedCopies.Select(mail => mail.FolderId).Should().BeEquivalentTo([_inboxFolder.Id, archiveFolder.Id]);
}
[Fact]
public async Task UpdateMailAsync_PreservesLocalPinnedState()
{
var existingMail = BuildMail(_inboxFolder.Id, DateTime.UtcNow.AddHours(-1));
existingMail.IsPinned = true;
await _databaseService.Connection.InsertAsync(existingMail, typeof(MailCopy));
var refreshedMail = BuildMail(_inboxFolder.Id, DateTime.UtcNow, id: existingMail.Id);
refreshedMail.UniqueId = existingMail.UniqueId;
refreshedMail.FileId = existingMail.FileId;
refreshedMail.Subject = "Updated subject";
await _mailService.UpdateMailAsync(refreshedMail);
var storedMail = await _databaseService.Connection.FindAsync<MailCopy>(existingMail.UniqueId);
storedMail.Should().NotBeNull();
storedMail!.IsPinned.Should().BeTrue();
storedMail.Subject.Should().Be("Updated subject");
}
// ── Performance: 1 000 mails / ~70 threads ─────────────────────────────────
/// <summary>
@@ -327,6 +327,94 @@ public class MailThreadingTests : IAsyncLifetime
}
}
[Fact]
public async Task ChangePinnedStatusAsync_SendsHydratedBulkMailUpdatedMessage()
{
var mail = new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = _draftFolder.Id,
IsPinned = false,
Subject = "Pinned draft"
};
await _databaseService.Connection.InsertAsync(mail, typeof(MailCopy));
var recipient = new MailUpdateRecipient();
WeakReferenceMessenger.Default.Register<MailUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Register<BulkMailUpdatedMessage>(recipient);
try
{
await _mailService.ChangePinnedStatusAsync([mail.UniqueId], true);
recipient.SingleUpdates.Should().BeEmpty();
recipient.BulkUpdates.Should().ContainSingle();
recipient.BulkUpdates[0].ChangedProperties.Should().Be(MailCopyChangeFlags.IsPinned);
recipient.BulkUpdates[0].UpdatedMails.Should().ContainSingle();
var updatedMail = recipient.BulkUpdates[0].UpdatedMails[0];
updatedMail.IsPinned.Should().BeTrue();
updatedMail.AssignedFolder.Should().NotBeNull();
updatedMail.AssignedFolder!.Id.Should().Be(_draftFolder.Id);
updatedMail.AssignedAccount.Should().NotBeNull();
updatedMail.AssignedAccount!.Id.Should().Be(_account.Id);
}
finally
{
WeakReferenceMessenger.Default.Unregister<MailUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Unregister<BulkMailUpdatedMessage>(recipient);
}
}
[Fact]
public async Task CreateAssignmentAsync_SendsHydratedMailAddedMessage()
{
var archiveFolder = new MailItemFolder
{
Id = Guid.NewGuid(),
MailAccountId = _account.Id,
FolderName = "Archive",
RemoteFolderId = "archive",
SpecialFolderType = SpecialFolderType.Archive,
IsSystemFolder = true,
IsSynchronizationEnabled = true
};
var mail = new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = "assignment-mail",
FolderId = _draftFolder.Id,
Subject = "Assigned copy"
};
await _databaseService.Connection.InsertAsync(archiveFolder, typeof(MailItemFolder));
await _databaseService.Connection.InsertAsync(mail, typeof(MailCopy));
var recipient = new MailAddRecipient();
WeakReferenceMessenger.Default.Register<MailAddedMessage>(recipient);
try
{
await _mailService.CreateAssignmentAsync(_account.Id, mail.Id, archiveFolder.RemoteFolderId);
recipient.Added.Should().ContainSingle();
var addedMail = recipient.Added[0].AddedMail;
addedMail.UniqueId.Should().NotBe(mail.UniqueId);
addedMail.AssignedFolder.Should().NotBeNull();
addedMail.AssignedFolder!.Id.Should().Be(archiveFolder.Id);
addedMail.AssignedAccount.Should().NotBeNull();
addedMail.AssignedAccount!.Id.Should().Be(_account.Id);
}
finally
{
WeakReferenceMessenger.Default.Unregister<MailAddedMessage>(recipient);
}
}
private static MimeMessage CreateReferencedMimeMessage(string subject, string? messageId = null)
{
var message = new MimeMessage();
@@ -350,6 +438,13 @@ public class MailThreadingTests : IAsyncLifetime
public void Receive(BulkMailUpdatedMessage message) => BulkUpdates.Add(message);
}
internal sealed class MailAddRecipient : IRecipient<MailAddedMessage>
{
public List<MailAddedMessage> Added { get; } = [];
public void Receive(MailAddedMessage message) => Added.Add(message);
}
internal sealed class MailReadStatusRecipient : IRecipient<MailReadStatusChanged>, IRecipient<BulkMailReadStatusChanged>
{
public List<MailReadStatusChanged> SingleUpdates { get; } = [];