From 331b9665565a3a9cc22004c759291cdb69131479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 7 Feb 2026 14:03:41 +0100 Subject: [PATCH] Info panel for synchronizers in shell. --- .../SynchronizationActionItem.cs | 15 +++ .../Translations/en_US/resources.json | 16 +++ .../Helpers/SynchronizationActionHelper.cs | 108 ++++++++++++++++++ Wino.Core/Services/WinoRequestDelegator.cs | 48 ++++++-- Wino.Core/Synchronizers/WinoSynchronizer.cs | 4 + Wino.Mail.WinUI/ShellWindow.xaml | 56 ++++++++- Wino.Mail.WinUI/ShellWindow.xaml.cs | 56 ++++++++- .../UI/SynchronizationActionsAdded.cs | 14 +++ .../UI/SynchronizationActionsCompleted.cs | 9 ++ 9 files changed, 314 insertions(+), 12 deletions(-) create mode 100644 Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs create mode 100644 Wino.Core/Helpers/SynchronizationActionHelper.cs create mode 100644 Wino.Messages/UI/SynchronizationActionsAdded.cs create mode 100644 Wino.Messages/UI/SynchronizationActionsCompleted.cs diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs new file mode 100644 index 00000000..655e8918 --- /dev/null +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationActionItem.cs @@ -0,0 +1,15 @@ +using System; + +namespace Wino.Core.Domain.Models.Synchronization; + +/// +/// Represents a single grouped synchronization action displayed in the sync status flyout. +/// For example: "Deleting 3 mail(s)" or "Marking folder as read". +/// +public class SynchronizationActionItem +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid AccountId { get; set; } + public string AccountName { get; set; } + public string Description { get; set; } +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 613221ac..4ba0ed11 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -84,6 +84,22 @@ "Buttons_Yes": "Yes", "Sync_SynchronizingFolder": "Synchronizing {0} {1}%", "Sync_DownloadedMessages": "Downloaded {0} messages from {1}", + "SyncAction_Archiving": "Archiving {0} mail(s)", + "SyncAction_ClearingFlag": "Unflagging {0} mail(s)", + "SyncAction_CreatingDraft": "Creating draft", + "SyncAction_Deleting": "Deleting {0} mail(s)", + "SyncAction_EmptyingFolder": "Emptying folder", + "SyncAction_MarkingAsRead": "Marking {0} mail(s) as read", + "SyncAction_MarkingAsUnread": "Marking {0} mail(s) as unread", + "SyncAction_MarkingFolderAsRead": "Marking folder as read", + "SyncAction_Moving": "Moving {0} mail(s)", + "SyncAction_MovingToFocused": "Moving {0} mail(s) to Focused", + "SyncAction_RenamingFolder": "Renaming folder", + "SyncAction_SendingMail": "Sending mail", + "SyncAction_SettingFlag": "Flagging {0} mail(s)", + "SyncAction_SynchronizingAccount": "Synchronizing {0}", + "SyncAction_SynchronizingAccounts": "Synchronizing {0} account(s)", + "SyncAction_Unarchiving": "Unarchiving {0} mail(s)", "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", diff --git a/Wino.Core/Helpers/SynchronizationActionHelper.cs b/Wino.Core/Helpers/SynchronizationActionHelper.cs new file mode 100644 index 00000000..87985236 --- /dev/null +++ b/Wino.Core/Helpers/SynchronizationActionHelper.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Requests.Folder; +using Wino.Core.Requests.Mail; + +namespace Wino.Core.Helpers; + +/// +/// Converts queued synchronization requests into user-facing action descriptions. +/// +public static class SynchronizationActionHelper +{ + public static List CreateActionItems( + IEnumerable requests, Guid accountId, string accountName) + { + var items = new List(); + + // Group mail action requests by operation + var mailRequests = requests.OfType(); + var mailGroups = mailRequests.GroupBy(r => GetMailActionKey(r)); + + foreach (var group in mailGroups) + { + var description = GetMailActionDescription(group.Key, group.ToList()); + + if (description != null) + { + items.Add(new SynchronizationActionItem + { + AccountId = accountId, + AccountName = accountName, + Description = description + }); + } + } + + // Handle folder action requests individually + var folderRequests = requests.OfType(); + foreach (var folderRequest in folderRequests) + { + var description = GetFolderActionDescription(folderRequest); + + if (description != null) + { + items.Add(new SynchronizationActionItem + { + AccountId = accountId, + AccountName = accountName, + Description = description + }); + } + } + + return items; + } + + /// + /// Returns a key that differentiates MarkRead vs MarkUnread, Flag vs Unflag, Archive vs Unarchive. + /// + private static string GetMailActionKey(IMailActionRequest request) + { + return request switch + { + MarkReadRequest r => r.IsRead ? "MarkRead" : "MarkUnread", + ChangeFlagRequest r => r.IsFlagged ? "SetFlag" : "ClearFlag", + ArchiveRequest r => r.IsArchiving ? "Archive" : "Unarchive", + _ => request.Operation.ToString() + }; + } + + private static string GetMailActionDescription(string actionKey, List requests) + { + int count = requests.Count; + + return actionKey switch + { + "MarkRead" => string.Format(Translator.SyncAction_MarkingAsRead, count), + "MarkUnread" => string.Format(Translator.SyncAction_MarkingAsUnread, count), + "Delete" => string.Format(Translator.SyncAction_Deleting, count), + "Move" => string.Format(Translator.SyncAction_Moving, count), + "Archive" => string.Format(Translator.SyncAction_Archiving, count), + "Unarchive" => string.Format(Translator.SyncAction_Unarchiving, count), + "SetFlag" => string.Format(Translator.SyncAction_SettingFlag, count), + "ClearFlag" => string.Format(Translator.SyncAction_ClearingFlag, count), + "CreateDraft" => Translator.SyncAction_CreatingDraft, + "Send" => Translator.SyncAction_SendingMail, + "MoveToFocused" => string.Format(Translator.SyncAction_MovingToFocused, count), + "AlwaysMoveTo" => string.Format(Translator.SyncAction_Moving, count), + _ => null + }; + } + + private static string GetFolderActionDescription(IFolderActionRequest request) + { + return request switch + { + RenameFolderRequest => Translator.SyncAction_RenamingFolder, + EmptyFolderRequest => Translator.SyncAction_EmptyingFolder, + MarkFolderAsReadRequest => Translator.SyncAction_MarkingFolderAsRead, + _ => null + }; + } +} diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index 693826b1..35ca5edc 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -13,9 +13,11 @@ using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Helpers; using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Mail; using Wino.Messaging.Server; +using Wino.Messaging.UI; namespace Wino.Core.Services; @@ -24,14 +26,17 @@ public class WinoRequestDelegator : IWinoRequestDelegator private readonly IWinoRequestProcessor _winoRequestProcessor; private readonly IFolderService _folderService; private readonly IMailDialogService _dialogService; + private readonly IAccountService _accountService; public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor, IFolderService folderService, - IMailDialogService dialogService) + IMailDialogService dialogService, + IAccountService accountService) { _winoRequestProcessor = winoRequestProcessor; _folderService = folderService; _dialogService = dialogService; + _accountService = accountService; } public async Task ExecuteAsync(MailOperationPreperationRequest request) @@ -82,14 +87,20 @@ public class WinoRequestDelegator : IWinoRequestDelegator var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id); // Queue requests for each account and start synchronization. - foreach (var accountId in accountIds) + foreach (var accountGroup in accountIds) { - foreach (var accountRequest in accountId) + foreach (var accountRequest in accountGroup) { - await QueueRequestAsync(accountRequest, accountId.Key); + await QueueRequestAsync(accountRequest, accountGroup.Key); } - await QueueSynchronizationAsync(accountId.Key); + var account = accountGroup.First().Item.AssignedAccount; + var actionItems = SynchronizationActionHelper.CreateActionItems(accountGroup, accountGroup.Key, account.Name); + + if (actionItems.Count > 0) + WeakReferenceMessenger.Default.Send(new SynchronizationActionsAdded(accountGroup.Key, account.Name, actionItems)); + + await QueueSynchronizationAsync(accountGroup.Key); } } @@ -117,23 +128,28 @@ public class WinoRequestDelegator : IWinoRequestDelegator if (request == null) return; await QueueRequestAsync(request, accountId); + await SendSyncActionsAddedAsync([request], accountId); await QueueSynchronizationAsync(accountId); } public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest) { var request = new CreateDraftRequest(draftPreperationRequest); + var accountId = draftPreperationRequest.Account.Id; - await QueueRequestAsync(request, draftPreperationRequest.Account.Id); - await QueueSynchronizationAsync(draftPreperationRequest.Account.Id); + await QueueRequestAsync(request, accountId); + await SendSyncActionsAddedAsync([request], accountId, draftPreperationRequest.Account.Name); + await QueueSynchronizationAsync(accountId); } public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest) { var request = new SendDraftRequest(sendDraftPreperationRequest); + var account = sendDraftPreperationRequest.MailItem.AssignedAccount; - await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id); - await QueueSynchronizationAsync(sendDraftPreperationRequest.MailItem.AssignedAccount.Id); + await QueueRequestAsync(request, account.Id); + await SendSyncActionsAddedAsync([request], account.Id, account.Name); + await QueueSynchronizationAsync(account.Id); } public async Task ExecuteAsync(CalendarOperationPreparationRequest calendarPreparationRequest) @@ -187,6 +203,20 @@ public class WinoRequestDelegator : IWinoRequestDelegator return Task.CompletedTask; } + private async Task SendSyncActionsAddedAsync(IEnumerable requests, Guid accountId, string accountName = null) + { + if (accountName == null) + { + var account = await _accountService.GetAccountAsync(accountId); + accountName = account?.Name ?? string.Empty; + } + + var actionItems = SynchronizationActionHelper.CreateActionItems(requests, accountId, accountName); + + if (actionItems.Count > 0) + WeakReferenceMessenger.Default.Send(new SynchronizationActionsAdded(accountId, accountName, actionItems)); + } + private Task QueueCalendarSynchronizationAsync(Guid accountId) { var options = new CalendarSynchronizationOptions() diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs index 432ac1e6..ecd96502 100644 --- a/Wino.Core/Synchronizers/WinoSynchronizer.cs +++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs @@ -215,6 +215,8 @@ public abstract class WinoSynchronizer a.ResynchronizationDelay > 0); diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml index 3ae6a173..23fa2770 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml +++ b/Wino.Mail.WinUI/ShellWindow.xaml @@ -9,6 +9,7 @@ xmlns:local="using:Wino.Mail.WinUI" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:notifyicon="using:H.NotifyIcon" + xmlns:syncModels="using:Wino.Core.Domain.Models.Synchronization" xmlns:winuiex="using:WinUIEx" Title="ShellWindow" mc:Ignorable="d"> @@ -34,7 +35,58 @@ IsPaneToggleButtonVisible="True" PaneToggleRequested="PaneButtonClicked"> - + + + + @@ -53,7 +105,7 @@ - + diff --git a/Wino.Mail.WinUI/ShellWindow.xaml.cs b/Wino.Mail.WinUI/ShellWindow.xaml.cs index 2d47b747..2c7f6b9d 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml.cs +++ b/Wino.Mail.WinUI/ShellWindow.xaml.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.ObjectModel; +using System.Linq; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; @@ -7,7 +9,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Windows.UI; +using Wino.Core.Domain; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Synchronization; using Wino.Mail.WinUI.Interfaces; using Wino.Messaging.Client.Shell; using Wino.Messaging.UI; @@ -16,7 +20,11 @@ using WinUIEx; namespace Wino.Mail.WinUI; -public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient, IRecipient +public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, + IRecipient, + IRecipient, + IRecipient, + IRecipient { public IStatePersistanceService StatePersistanceService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception("StatePersistanceService not registered in DI container."); public IPreferencesService PreferencesService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception("PreferencesService not registered in DI container."); @@ -25,6 +33,8 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient public ICommand ShowWinoCommand { get; set; } public ICommand ExitWinoCommand { get; set; } + public ObservableCollection SyncActionItems { get; } = new(); + public ShellWindow() { RegisterRecipients(); @@ -172,6 +182,46 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient UpdateTitleBarColors(message.IsUnderlyingThemeDark); } + public void Receive(SynchronizationActionsAdded message) + { + DispatcherQueue.TryEnqueue(() => + { + foreach (var action in message.Actions) + SyncActionItems.Add(action); + + UpdateSyncStatusVisibility(); + }); + } + + public void Receive(SynchronizationActionsCompleted message) + { + DispatcherQueue.TryEnqueue(() => + { + var toRemove = SyncActionItems.Where(a => a.AccountId == message.AccountId).ToList(); + + foreach (var item in toRemove) + SyncActionItems.Remove(item); + + UpdateSyncStatusVisibility(); + }); + } + + private void UpdateSyncStatusVisibility() + { + SyncStatusButton.Visibility = SyncActionItems.Any() + ? Visibility.Visible + : Visibility.Collapsed; + + var distinctAccounts = SyncActionItems.Select(a => a.AccountId).Distinct().Count(); + + SyncStatusText.Text = distinctAccounts switch + { + 0 => string.Empty, + 1 => string.Format(Translator.SyncAction_SynchronizingAccount, SyncActionItems.First().AccountName), + _ => string.Format(Translator.SyncAction_SynchronizingAccounts, distinctAccounts) + }; + } + private void UpdateTitleBarColors(bool isDarkTheme) { DispatcherQueue.TryEnqueue(() => @@ -250,12 +300,16 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient { WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } private void UnregisterRecipients() { WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); } private void SegmentedChanged(object sender, SelectionChangedEventArgs e) diff --git a/Wino.Messages/UI/SynchronizationActionsAdded.cs b/Wino.Messages/UI/SynchronizationActionsAdded.cs new file mode 100644 index 00000000..7655d884 --- /dev/null +++ b/Wino.Messages/UI/SynchronizationActionsAdded.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Messaging.UI; + +/// +/// Sent when synchronization requests are queued for an account. +/// Contains grouped action descriptions for the UI to display. +/// +public record SynchronizationActionsAdded( + Guid AccountId, + string AccountName, + List Actions) : UIMessageBase; diff --git a/Wino.Messages/UI/SynchronizationActionsCompleted.cs b/Wino.Messages/UI/SynchronizationActionsCompleted.cs new file mode 100644 index 00000000..e5a04b39 --- /dev/null +++ b/Wino.Messages/UI/SynchronizationActionsCompleted.cs @@ -0,0 +1,9 @@ +using System; + +namespace Wino.Messaging.UI; + +/// +/// Sent when all queued synchronization requests for an account have been executed. +/// +public record SynchronizationActionsCompleted( + Guid AccountId) : UIMessageBase;