From 7f198bad9206bb8778df3c54d1ddebc9be591e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 21 Feb 2026 10:53:39 +0100 Subject: [PATCH] Implement mail and calendar item synchronizer state (#815) * Track pending sync operations per mail/calendar item * Updated progressbar for in progress drafts --- .../Interfaces/IBaseSynchronizer.cs | 15 ++++- Wino.Core/Synchronizers/BaseSynchronizer.cs | 45 +++++++++++++- Wino.Core/Synchronizers/WinoSynchronizer.cs | 18 +++++- Wino.Mail.ViewModels/ComposePageViewModel.cs | 60 ++++++++++++++----- Wino.Mail.WinUI/Views/Mail/ComposePage.xaml | 8 +++ 5 files changed, 126 insertions(+), 20 deletions(-) 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"> + + + +