Implement mail and calendar item synchronizer state (#815)

* Track pending sync operations per mail/calendar item

* Updated progressbar for in progress drafts
This commit is contained in:
Burak Kaan Köse
2026-02-21 10:53:39 +01:00
committed by GitHub
parent a912ada890
commit 7f198bad92
5 changed files with 126 additions and 20 deletions
@@ -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
/// <param name="request">Request to queue.</param>
void QueueRequest(IRequestBase request);
/// <summary>
/// Returns whether there is an in-progress (queued or currently executing) operation for the given mail unique id.
/// </summary>
/// <param name="mailUniqueId">Mail unique id to check.</param>
bool HasPendingOperation(Guid mailUniqueId);
/// <summary>
/// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
/// </summary>
/// <param name="calendarItemId">Calendar item id to check.</param>
bool HasPendingCalendarOperation(Guid calendarItemId);
/// <summary>
/// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated.
+44 -1
View File
@@ -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<TBaseRequest> : ObservableObject,
protected CancellationToken activeSynchronizationCancellationToken;
protected List<IRequestBase> changeRequestQueue = [];
private readonly ConcurrentDictionary<Guid, byte> _pendingMailOperationIds = new();
private readonly ConcurrentDictionary<Guid, byte> _pendingCalendarOperationIds = new();
protected readonly IMessenger Messenger;
public MailAccount Account { get; }
@@ -119,7 +122,47 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
/// Queues a single request to be executed in the next synchronization.
/// </summary>
/// <param name="request">Request to execute.</param>
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<IRequestBase> requests)
{
foreach (var request in requests)
UntrackProcessedRequest(request);
}
/// <summary>
/// Runs existing queued requests in the queue.
+16 -2
View File
@@ -219,7 +219,14 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
Console.WriteLine($"Prepared {nativeRequests.Count()} native requests");
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken).ConfigureAwait(false);
try
{
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken).ConfigureAwait(false);
}
finally
{
UntrackProcessedRequests(requestCopies);
}
Messenger.Send(new SynchronizationActionsCompleted(Account.Id));
@@ -419,7 +426,14 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
Console.WriteLine($"Prepared {nativeRequests.Count()} native calendar requests");
await ExecuteNativeRequestsAsync(nativeRequests, cancellationToken).ConfigureAwait(false);
try
{
await ExecuteNativeRequestsAsync(nativeRequests, cancellationToken).ConfigureAwait(false);
}
finally
{
UntrackProcessedRequests(requestCopies);
}
Messenger.Send(new SynchronizationActionsCompleted(Account.Id));
+44 -16
View File
@@ -35,7 +35,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
// Update is triggered when we leave the page.
private bool isUpdatingMimeBlocked = false;
private bool canSendMail => 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<MailAccountAlias> 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();
});
}
}
@@ -190,6 +190,14 @@
CommandAlignment="Right"
IsDynamicOverflowEnabled="True"
OverflowButtonAlignment="Left">
<AppBarButton
MinWidth="40"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}">
<ProgressRing IsActive="True" />
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"