From 1ce7bb8c02ef7a8d88c4f223e5680df5d161d71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 27 Nov 2024 02:51:07 +0100 Subject: [PATCH] Seperation of base synchronizer. --- .../Synchronizers/BaseMailSynchronizer.cs | 212 +----------------- Wino.Core/Synchronizers/BaseSynchronizer.cs | 113 ++++++++++ .../Synchronizers/Mail/ImapSynchronizer.cs | 6 +- Wino.Mail/Views/MailListPage.xaml | 2 +- Wino.Mail/Views/PersonalizationPage.xaml | 2 +- 5 files changed, 124 insertions(+), 211 deletions(-) create mode 100644 Wino.Core/Synchronizers/BaseSynchronizer.cs diff --git a/Wino.Core/Synchronizers/BaseMailSynchronizer.cs b/Wino.Core/Synchronizers/BaseMailSynchronizer.cs index d2f5a21c..f68b1cd1 100644 --- a/Wino.Core/Synchronizers/BaseMailSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseMailSynchronizer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; @@ -24,14 +23,14 @@ using Wino.Messaging.UI; namespace Wino.Core.Synchronizers { - public abstract class BaseMailSynchronizer : IBaseMailSynchronizer + public abstract class BaseMailSynchronizer : BaseSynchronizer, IBaseMailSynchronizer { - private SemaphoreSlim synchronizationSemaphore = new(1); - private CancellationToken activeSynchronizationCancellationToken; - - protected List changeRequestQueue = []; protected ILogger Logger = Log.ForContext>(); + protected BaseMailSynchronizer(MailAccount account) : base(account) + { + } + /// /// How many items per single HTTP call can be modified. /// @@ -42,31 +41,6 @@ namespace Wino.Core.Synchronizers /// public abstract uint InitialMessageDownloadCountPerFolder { get; } - protected BaseMailSynchronizer(MailAccount account) - { - Account = account; - } - - public MailAccount Account { get; } - - private AccountSynchronizerState state; - public AccountSynchronizerState State - { - get { return state; } - private set - { - state = value; - - WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value)); - } - } - - /// - /// Queues a single request to be executed in the next synchronization. - /// - /// Request to execute. - public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request); - /// /// Creates a new Wino Mail Item package out of native message type with full Mime. /// @@ -75,39 +49,13 @@ namespace Wino.Core.Synchronizers /// Package that encapsulates downloaded Mime and additional information for adding new mail. public abstract Task> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default); - /// - /// Runs existing queued requests in the queue. - /// - /// Batched requests to execute. Integrator methods will only receive batched requests. - /// Cancellation token - public abstract Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default); - - /// - /// Refreshes remote mail account profile if possible. - /// Profile picture, sender name and mailbox settings (todo) will be handled in this step. - /// - public virtual Task GetProfileInformationAsync() => default; - /// /// Refreshes the aliases of the account. /// Only available for Gmail right now. /// protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask; - /// - /// Returns the base64 encoded profile picture of the account from the given URL. - /// - /// URL to retrieve picture from. - /// base64 encoded profile picture - protected async Task GetProfilePictureBase64EncodedAsync(string url) - { - using var client = new HttpClient(); - var response = await client.GetAsync(url).ConfigureAwait(false); - var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - - return Convert.ToBase64String(byteContent); - } /// /// Internally synchronizes the account with the given options. @@ -118,27 +66,7 @@ namespace Wino.Core.Synchronizers /// Synchronization result that contains summary of the sync. protected abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); - /// - /// Safely updates account's profile information. - /// Database changes are reflected after this call. - /// - private async Task SynchronizeProfileInformationInternalAsync() - { - var profileInformation = await GetProfileInformationAsync(); - if (profileInformation != null) - { - Account.SenderName = profileInformation.SenderName; - Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData; - - if (!string.IsNullOrEmpty(profileInformation.AccountAddress)) - { - Account.Address = profileInformation.AccountAddress; - } - } - - return profileInformation; - } /// /// Batches network requests, executes them, and does the needed synchronization after the batch request execution. @@ -338,110 +266,6 @@ namespace Wino.Core.Synchronizers public void PublishSynchronizationProgress(double progress) => WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress)); - /// - /// 1. Group all requests by operation type. - /// 2. Group all individual operation type requests with equality check. - /// Equality comparison in the records are done with RequestComparer - /// to ignore Item property. Each request can have their own logic for comparison. - /// For example, move requests for different mails from the same folder to the same folder - /// must be dispatched in the same batch. This is much faster for the server. Specially IMAP - /// since all folders must be asynchronously opened/closed. - /// - /// Batch request collection for all these single requests. - //private List CreateBatchRequests() - //{ - // var batchList = new List(); - // var comparer = new RequestComparer(); - - // while (changeRequestQueue.Count > 0) - // { - // if (changeRequestQueue.TryPeek(out IRequestBase request)) - // { - // // Mail request, must be batched. - // if (request is IMailActionRequest mailRequest) - // { - // var equalItems = changeRequestQueue - // .Where(a => a is IMailActionRequest && comparer.Equals(a, request)) - // .Cast() - // .ToList(); - - // batchList.Add(mailRequest.CreateBatch(equalItems)); - - // // Remove these items from the queue. - // foreach (var item in equalItems) - // { - // changeRequestQueue.TryTake(out _); - // } - // } - // else if (changeRequestQueue.TryTake(out request)) - // { - // // This is a folder operation. - // // There is no need to batch them since Users can't do folder ops in bulk. - - // batchList.Add(request); - // } - // } - // } - - // return batchList; - //} - - /// - /// Converts batched requests into HTTP/Task calls that derived synchronizers can execute. - /// - /// Batch requests to be converted. - /// Collection of native requests for individual synchronizer type. - //private IEnumerable> CreateNativeRequestBundles(IEnumerable batchChangeRequests) - //{ - // IEnumerable>> GetNativeRequests() - // { - // foreach (var item in batchChangeRequests) - // { - // switch (item.Operation) - // { - // case MailSynchronizerOperation.Send: - // yield return SendDraft((BatchSendDraftRequestRequest)item); - // break; - // case MailSynchronizerOperation.MarkRead: - // yield return MarkRead((BatchMarkReadRequest)item); - // break; - // case MailSynchronizerOperation.Move: - // yield return Move((BatchMoveRequest)item); - // break; - // case MailSynchronizerOperation.Delete: - // yield return Delete((BatchDeleteRequest)item); - // break; - // case MailSynchronizerOperation.ChangeFlag: - // yield return ChangeFlag((BatchChangeFlagRequest)item); - // break; - // case MailSynchronizerOperation.AlwaysMoveTo: - // yield return AlwaysMoveTo((BatchAlwaysMoveToRequest)item); - // break; - // case MailSynchronizerOperation.MoveToFocused: - // yield return MoveToFocused((BatchMoveToFocusedRequest)item); - // break; - // case MailSynchronizerOperation.CreateDraft: - // yield return CreateDraft((BatchCreateDraftRequest)item); - // break; - // case MailSynchronizerOperation.RenameFolder: - // yield return RenameFolder((RenameFolderRequest)item); - // break; - // case MailSynchronizerOperation.EmptyFolder: - // yield return EmptyFolder((EmptyFolderRequest)item); - // break; - // case MailSynchronizerOperation.MarkFolderRead: - // yield return MarkFolderAsRead((MarkFolderAsReadRequest)item); - // break; - // case MailSynchronizerOperation.Archive: - // yield return Archive((BatchArchiveRequest)item); - // break; - // } - // } - // }; - - // return GetNativeRequests().SelectMany(collections => collections); - //} - /// /// Attempts to find out the best possible synchronization options after the batch request execution. /// @@ -486,12 +310,10 @@ namespace Wino.Core.Synchronizers public virtual List> CreateDraft(CreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> SendDraft(SendDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); - public virtual List> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); - /// /// Downloads a single missing message from synchronizer and saves it to given FileId from IMailItem. /// @@ -500,27 +322,7 @@ namespace Wino.Core.Synchronizers /// Cancellation token. public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); - public bool CancelActiveSynchronization() - { - // TODO: What if account is deleted during synchronization? - return true; - } - - #region Bundle Helpers - - public List> ForEachRequest(IEnumerable requests, - Func action) - where TWinoRequestType : IRequestBase - { - List> ret = []; - - foreach (var request in requests) - ret.Add(new HttpRequestBundle(action(request), request)); - - return ret; - } - - public List> CreateSingleBundle(Func action, IRequestBase request, IUIChangeRequest uIChangeRequest) + public List> CreateSingleTaskBundle(Func action, IRequestBase request, IUIChangeRequest uIChangeRequest) { return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)]; } @@ -538,7 +340,5 @@ namespace Wino.Core.Synchronizers return ret; } - - #endregion } } diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs new file mode 100644 index 00000000..b837165a --- /dev/null +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Requests.Bundles; +using Wino.Messaging.UI; + +namespace Wino.Core.Synchronizers +{ + public abstract class BaseSynchronizer + { + protected SemaphoreSlim synchronizationSemaphore = new(1); + protected CancellationToken activeSynchronizationCancellationToken; + + protected List changeRequestQueue = []; + public MailAccount Account { get; } + + private AccountSynchronizerState state; + public AccountSynchronizerState State + { + get { return state; } + set + { + state = value; + + WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value)); + } + } + + protected BaseSynchronizer(MailAccount account) + { + Account = account; + } + + + /// + /// Queues a single request to be executed in the next synchronization. + /// + /// Request to execute. + public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request); + + /// + /// Runs existing queued requests in the queue. + /// + /// Batched requests to execute. Integrator methods will only receive batched requests. + /// Cancellation token + public abstract Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default); + + // TODO: What if account is deleted during synchronization? + public bool CancelActiveSynchronization() => true; + + /// + /// Refreshes remote mail account profile if possible. + /// Profile picture, sender name and mailbox settings (todo) will be handled in this step. + /// + public virtual Task GetProfileInformationAsync() => default; + + /// + /// Safely updates account's profile information. + /// Database changes are reflected after this call. + /// + protected async Task SynchronizeProfileInformationInternalAsync() + { + var profileInformation = await GetProfileInformationAsync(); + + if (profileInformation != null) + { + Account.SenderName = profileInformation.SenderName; + Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData; + + if (!string.IsNullOrEmpty(profileInformation.AccountAddress)) + { + Account.Address = profileInformation.AccountAddress; + } + } + + return profileInformation; + } + + /// + /// Returns the base64 encoded profile picture of the account from the given URL. + /// + /// URL to retrieve picture from. + /// base64 encoded profile picture + protected async Task GetProfilePictureBase64EncodedAsync(string url) + { + using var client = new HttpClient(); + + var response = await client.GetAsync(url).ConfigureAwait(false); + var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + + return Convert.ToBase64String(byteContent); + } + + public List> ForEachRequest(IEnumerable requests, + Func action) + where TWinoRequestType : IRequestBase + { + List> ret = []; + + foreach (var request in requests) + ret.Add(new HttpRequestBundle(action(request), request)); + + return ret; + } + } +} diff --git a/Wino.Core/Synchronizers/Mail/ImapSynchronizer.cs b/Wino.Core/Synchronizers/Mail/ImapSynchronizer.cs index 943790bb..3a807d35 100644 --- a/Wino.Core/Synchronizers/Mail/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/Mail/ImapSynchronizer.cs @@ -287,7 +287,7 @@ namespace Wino.Core.Synchronizers.Mail public override List> CreateDraft(CreateDraftRequest request) { - return CreateSingleBundle(async (client, item) => + return CreateSingleTaskBundle(async (client, item) => { var remoteDraftFolder = await client.GetFolderAsync(request.DraftPreperationRequest.CreatedLocalDraftCopy.AssignedFolder.RemoteFolderId).ConfigureAwait(false); @@ -312,7 +312,7 @@ namespace Wino.Core.Synchronizers.Mail public override List> SendDraft(SendDraftRequest request) { - return CreateSingleBundle(async (client, item) => + return CreateSingleTaskBundle(async (client, item) => { // Batch sending is not supported. It will always be a single request therefore no need for a loop here. @@ -388,7 +388,7 @@ namespace Wino.Core.Synchronizers.Mail public override List> RenameFolder(RenameFolderRequest request) { - return CreateSingleBundle(async (client, item) => + return CreateSingleTaskBundle(async (client, item) => { var folder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false); await folder.RenameAsync(folder.ParentFolder, request.NewFolderName).ConfigureAwait(false); diff --git a/Wino.Mail/Views/MailListPage.xaml b/Wino.Mail/Views/MailListPage.xaml index ac72fa0f..b81b32fa 100644 --- a/Wino.Mail/Views/MailListPage.xaml +++ b/Wino.Mail/Views/MailListPage.xaml @@ -506,7 +506,7 @@ - +