Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/BetterMail.App/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4110,10 +4110,15 @@ private async Task MoveMessagesAsync(
var displayed = Messages.FirstOrDefault(candidate => SameMessage(candidate, message));
var actualDestination = Folders.FirstOrDefault(folder => folder.MailboxId == message.MailboxId &&
(folder.ProviderId == destinationFolderId || folder.WellKnownName == destinationFolderId))?.ProviderId ?? destinationFolderId;
if (displayed?.FolderId == actualDestination ||
ConversationThread.SelectedMessage is { Message: var reading } && SameMessage(reading, message) && reading.FolderId == actualDestination)
continue;
if (displayed is not null && displayed.FolderId == message.FolderId)
// A body hydration can already contain the optimistic destination while
// the row still belongs to the source list. Only the actual navigation
// target determines whether the user has opened the destination folder.
var destinationIsOpen = _selectedFolder is not null
? _selectedFolder.MailboxId == message.MailboxId && _selectedFolder.ProviderId == actualDestination
: IsUnifiedSection && Folders.Any(folder => folder.MailboxId == message.MailboxId &&
folder.ProviderId == actualDestination && folder.WellKnownName == "inbox");
if (destinationIsOpen) continue;
if (displayed is not null && (displayed.FolderId == message.FolderId || displayed.FolderId == actualDestination))
{
var index = Messages.IndexOf(displayed);
var wasCurrent = SameMessage(SelectedMessage, displayed);
Expand Down
48 changes: 47 additions & 1 deletion tests/BetterMail.Tests/MainWindowViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,45 @@ public async Task MoveFeedbackIsImmediateAndDoesNotResetANewerSelectionOrQueueSa
finally { provider.MoveRelease.TrySetResult(); if (Directory.Exists(directory)) Directory.Delete(directory, true); }
}

[Fact]
public async Task MovingToInboxPreservesRowWhenUnifiedInboxOpensDuringFeedback()
{
var token = TestContext.Current.CancellationToken;
var directory = Path.Combine(Path.GetTempPath(), "bettermail-move-unified-" + Guid.NewGuid());
var provider = new RecordingProvider { MoveRelease = new(TaskCreationOptions.RunContinuationsAsynchronously) };
try
{
await using var store = new EncryptedMailStore(Path.Combine(directory, "mail.db"), Convert.ToHexString(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)));
await store.InitializeAsync(token);
var account = new MailAccount("microsoft365", "account", "tenant", "alex@work.example", "Alex", ProviderCapabilities.Mail);
var mailbox = new Mailbox(account.AccountId, account.EmailAddress, "Alex");
await store.SaveAccountAsync(account, token);
await store.SaveMailboxAsync(mailbox, token);
var inbox = new MailFolder(mailbox.Id, "actual-inbox-id", "Inbox", 0, 0, "inbox");
var archive = new MailFolder(mailbox.Id, "archive", "Archive", 0, 0, "archive");
await store.SaveFoldersAsync(mailbox.Id, [inbox, archive], token);
provider.FolderResults = [inbox, archive];
var vm = new MainWindowViewModel(store, directory, _ => { }, _ => { }, null, provider);
vm.Accounts.Add(account); vm.Mailboxes.Add(mailbox);
vm.Folders.Add(new(inbox, "Alex")); vm.Folders.Add(new(archive, "Alex"));
var message = Message(mailbox.Id, "archive", "Restore to inbox", "Full body") with { IsRead = true };
await store.ApplySyncPageAsync("seed", new([message], null, false), token);
await ((AsyncCommand<MailFolderItem>)vm.SelectFolderCommand).ExecuteAsync(vm.Folders[1]);
var moving = vm.MoveSelectionToFolderAsync(vm.Folders[0]);
await WaitUntilAsync(() => vm.BusyActions.Any(action => action.Kind == MailActionKind.Move), token);
await ((AsyncCommand)vm.ShowUnifiedInboxCommand).ExecuteAsync();
Assert.False(moving.IsCompleted);
Assert.Equal(message.ProviderId, Assert.Single(vm.Messages).ProviderId);
await moving;
Assert.True(vm.IsUnifiedInbox);
Assert.Equal(message.ProviderId, Assert.Single(vm.Messages).ProviderId);
Assert.Equal(inbox.ProviderId, vm.Messages[0].FolderId);
provider.MoveRelease.TrySetResult();
await WaitUntilAsync(() => !vm.IsSyncing, token);
}
finally { provider.MoveRelease.TrySetResult(); if (Directory.Exists(directory)) Directory.Delete(directory, true); }
}

[Theory]
[InlineData("archive")]
[InlineData("deleteditems")]
Expand Down Expand Up @@ -1864,7 +1903,14 @@ await store.ApplySyncPageAsync(
await WaitUntilAsync(() => viewModel.IsUnifiedInbox && viewModel.Messages.Count == 2, cancellationToken);

provider.MoveRelease = new(TaskCreationOptions.RunContinuationsAsynchronously);
viewModel.DeleteCommand.Execute(null);
var deleting = ((AsyncCommand)viewModel.DeleteCommand).ExecuteAsync();
// Simulate body hydration observing the optimistic destination during
// the row's feedback interval, before it is removed from the source list.
await WaitUntilAsync(() => viewModel.BusyActions.Any(action => action.Kind == MailActionKind.Move), cancellationToken);
var deletingMessage = viewModel.SelectedMessage!;
typeof(MainWindowViewModel).GetMethod("ApplyMessageUpdate", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
.Invoke(viewModel, [deletingMessage, deletingMessage with { FolderId = "deleteditems" }]);
await deleting;
await WaitUntilAsync(() => provider.MoveDestination == "deleteditems", cancellationToken);
Assert.False(viewModel.IsMailActionRunning);
Assert.Single(viewModel.Messages);
Expand Down
Loading