diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
index 00f2f406..93620883 100644
--- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
+++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
@@ -23,6 +24,18 @@ public interface IBaseSynchronizer
/// Request to queue.
void QueueRequest(IRequestBase request);
+ ///
+ /// Returns whether there is an in-progress (queued or currently executing) operation for the given mail unique id.
+ ///
+ /// Mail unique id to check.
+ bool HasPendingOperation(Guid mailUniqueId);
+
+ ///
+ /// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
+ ///
+ /// Calendar item id to check.
+ bool HasPendingCalendarOperation(Guid calendarItemId);
+
///
/// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated.
diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs
index 500f14f9..5b1637c5 100644
--- a/Wino.Core/Synchronizers/BaseSynchronizer.cs
+++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
@@ -20,6 +21,8 @@ public abstract partial class BaseSynchronizer : ObservableObject,
protected CancellationToken activeSynchronizationCancellationToken;
protected List changeRequestQueue = [];
+ private readonly ConcurrentDictionary _pendingMailOperationIds = new();
+ private readonly ConcurrentDictionary _pendingCalendarOperationIds = new();
protected readonly IMessenger Messenger;
public MailAccount Account { get; }
@@ -119,7 +122,47 @@ public abstract partial class BaseSynchronizer : ObservableObject,
/// Queues a single request to be executed in the next synchronization.
///
/// Request to execute.
- public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request);
+ public void QueueRequest(IRequestBase request)
+ {
+ changeRequestQueue.Add(request);
+ TrackQueuedRequest(request);
+ }
+
+ public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId);
+
+ public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId);
+
+ protected void TrackQueuedRequest(IRequestBase request)
+ {
+ if (request is IMailActionRequest mailActionRequest)
+ {
+ _pendingMailOperationIds.TryAdd(mailActionRequest.Item.UniqueId, 0);
+ }
+
+ if (request is ICalendarActionRequest calendarActionRequest)
+ {
+ _pendingCalendarOperationIds.TryAdd(calendarActionRequest.Item.Id, 0);
+ }
+ }
+
+ protected void UntrackProcessedRequest(IRequestBase request)
+ {
+ if (request is IMailActionRequest mailActionRequest)
+ {
+ _pendingMailOperationIds.TryRemove(mailActionRequest.Item.UniqueId, out _);
+ }
+
+ if (request is ICalendarActionRequest calendarActionRequest)
+ {
+ _pendingCalendarOperationIds.TryRemove(calendarActionRequest.Item.Id, out _);
+ }
+ }
+
+ protected void UntrackProcessedRequests(IEnumerable requests)
+ {
+ foreach (var request in requests)
+ UntrackProcessedRequest(request);
+ }
///
/// Runs existing queued requests in the queue.
diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs
index bfb3f66f..9375611b 100644
--- a/Wino.Core/Synchronizers/WinoSynchronizer.cs
+++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs
@@ -219,7 +219,14 @@ public abstract class WinoSynchronizer ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null;
+ private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy;
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
@@ -52,24 +52,29 @@ public partial class ComposePageViewModel : MailBaseViewModel,
[NotifyPropertyChangedFor(nameof(IsLocalDraft))]
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
- private MailItemViewModel currentMailDraftItem;
-
- [ObservableProperty]
- private bool isImportanceSelected;
-
- [ObservableProperty]
- private MessageImportance selectedMessageImportance;
-
- [ObservableProperty]
- private bool isCCBCCVisible;
-
- [ObservableProperty]
- private string subject;
+ public partial MailItemViewModel CurrentMailDraftItem { get; set; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
- private MailAccount composingAccount;
+ public partial bool IsDraftBusy { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsImportanceSelected { get; set; }
+
+ [ObservableProperty]
+ public partial MessageImportance SelectedMessageImportance { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsCCBCCVisible { get; set; }
+
+ [ObservableProperty]
+ public partial string Subject { get; set; }
+
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
+ [NotifyCanExecuteChangedFor(nameof(SendCommand))]
+ public partial MailAccount ComposingAccount { get; set; }
[ObservableProperty]
public partial List AvailableAliases { get; set; }
@@ -312,6 +317,8 @@ public partial class ComposePageViewModel : MailBaseViewModel,
CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences,
base64EncodedMessage);
+ IsDraftBusy = true;
+
await _worker.ExecuteAsync(draftSendPreparationRequest);
}
@@ -430,6 +437,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
{
CurrentMailDraftItem = mailItem;
+ await UpdatePendingOperationStateAsync();
await TryPrepareComposeAsync(true);
}
}
@@ -446,6 +454,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
// Set the new draft item and prepare it.
CurrentMailDraftItem = message.MailItemViewModel;
+ await UpdatePendingOperationStateAsync();
await TryPrepareComposeAsync(true);
}
@@ -500,6 +509,23 @@ public partial class ComposePageViewModel : MailBaseViewModel,
return true;
}
+ private async Task UpdatePendingOperationStateAsync()
+ {
+ IsDraftBusy = false;
+
+ if (CurrentMailDraftItem?.MailCopy == null || !CurrentMailDraftItem.MailCopy.IsDraft)
+ return;
+
+ var accountId = CurrentMailDraftItem.MailCopy.AssignedAccount?.Id ?? Guid.Empty;
+
+ if (accountId == Guid.Empty)
+ return;
+
+ var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
+
+ IsDraftBusy = synchronizer?.HasPendingOperation(CurrentMailDraftItem.MailCopy.UniqueId) ?? false;
+ }
+
private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
{
if (CurrentMailDraftItem == null) return;
@@ -674,11 +700,13 @@ public partial class ComposePageViewModel : MailBaseViewModel,
if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId)
{
- await ExecuteUIThread(() =>
+ await ExecuteUIThread(async () =>
{
CurrentMailDraftItem.UpdateFrom(updatedMail);
DiscardCommand.NotifyCanExecuteChanged();
SendCommand.NotifyCanExecuteChanged();
+
+ await UpdatePendingOperationStateAsync();
});
}
}
diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
index 29f277ed..45c9b62a 100644
--- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
+++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
@@ -190,6 +190,14 @@
CommandAlignment="Right"
IsDynamicOverflowEnabled="True"
OverflowButtonAlignment="Left">
+
+
+
+