diff --git a/src/BetterMail.App/MainWindowViewModel.MessageActions.cs b/src/BetterMail.App/MainWindowViewModel.MessageActions.cs index a24465c..0aacee7 100644 --- a/src/BetterMail.App/MainWindowViewModel.MessageActions.cs +++ b/src/BetterMail.App/MainWindowViewModel.MessageActions.cs @@ -17,6 +17,8 @@ internal async Task DeleteDraftQuickAsync(LocalDraft draft) var current = await _store.GetLocalDraftAsync(draft.Id); if (current is null || current.IsQueued) return; await _store.QueueDraftDeletionAsync(current); + // A refresh started before this commit must not resurrect its stale snapshot. + Interlocked.Increment(ref _draftRefreshVersion); var action = (await _store.GetMailActionsAsync()).FirstOrDefault(item => item.Kind == MailActionKind.DeleteDraft && item.ItemId == current.Id); if (action is not null) ShowQueuedAction(action); await feedback; diff --git a/src/BetterMail.App/MainWindowViewModel.cs b/src/BetterMail.App/MainWindowViewModel.cs index 47e1bf0..6ce2821 100644 --- a/src/BetterMail.App/MainWindowViewModel.cs +++ b/src/BetterMail.App/MainWindowViewModel.cs @@ -1875,19 +1875,29 @@ DraftSyncStatus.UnsupportedAttachment or } } + private int _draftRefreshVersion; + private async Task RefreshDraftsAsync() { if (_store is null) { return; } + var version = Interlocked.Increment(ref _draftRefreshVersion); var drafts = await _store.GetLocalDraftSummariesAsync(); - Replace(Drafts, drafts.Where(static draft => !draft.IsQueued)); - Replace(Outbox, drafts.Where(static draft => draft.IsQueued && !draft.SendAccepted)); + if (!TryApplyDraftSnapshot(version, drafts)) return; await RefreshBusyActionsAsync(); RebuildVisibleDrafts(); } + private bool TryApplyDraftSnapshot(int version, IReadOnlyList drafts) + { + if (version != Volatile.Read(ref _draftRefreshVersion)) return false; + Replace(Drafts, drafts.Where(static draft => !draft.IsQueued)); + Replace(Outbox, drafts.Where(static draft => draft.IsQueued && !draft.SendAccepted)); + return true; + } + private async Task PrimeNewMailNotificationsAsync() { if (_store is null) diff --git a/tests/BetterMail.Tests/MainWindowViewModelTests.cs b/tests/BetterMail.Tests/MainWindowViewModelTests.cs index cdc5507..90efc6f 100644 --- a/tests/BetterMail.Tests/MainWindowViewModelTests.cs +++ b/tests/BetterMail.Tests/MainWindowViewModelTests.cs @@ -123,11 +123,15 @@ public async Task DraftQuickDeletionDoesNotWaitForOtherDraftsToSync() var second = first with { Id = "second", ProviderDraftId = "remote-second" }; await store.SaveLocalDraftAsync(first, token); await store.SaveLocalDraftAsync(second, token); vm.Drafts.Add(first); vm.Drafts.Add(second); + // Model a draft snapshot captured before deletion but delivered after both deletions. + var staleVersion = (int)typeof(MainWindowViewModel).GetField("_draftRefreshVersion", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(vm)!; var deleting = vm.DeleteDraftQuickAsync(first); Assert.True(vm.IsDraftDeletionPending(first)); await deleting.WaitAsync(TimeSpan.FromSeconds(5), token); await vm.DeleteDraftQuickAsync(second).WaitAsync(TimeSpan.FromSeconds(5), token); Assert.False(provider.DeleteRelease.Task.IsCompleted); + var applied = (bool)typeof(MainWindowViewModel).GetMethod("TryApplyDraftSnapshot", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.Invoke(vm, [staleVersion, new LocalDraft[] { first, second }])!; + Assert.False(applied); Assert.DoesNotContain(vm.Drafts, draft => draft.Id == first.Id || draft.Id == second.Id); Assert.True(await store.IsDraftPendingDeletionAsync(first.Id, token)); Assert.True(await store.IsDraftPendingDeletionAsync(second.Id, token));