From 3cc1d10b877c9fc1e9883073e545d134a05ade37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Fri, 31 Oct 2025 01:41:51 +0100 Subject: [PATCH] some changes for progress --- .../MenuItems/MergedAccountMenuItem.cs | 15 ++++- Wino.Core/Synchronizers/GmailSynchronizer.cs | 33 +++++++++- .../Collections/WinoMailCollection.cs | 5 +- Wino.Mail.ViewModels/MailListPageViewModel.cs | 37 +++++++++-- Wino.Mail.WinUI/AppShell.xaml | 64 +++++++++++++++---- .../Views/Settings/ContactsPage.xaml | 2 +- Wino.Services/DatabaseService.cs | 9 ++- Wino.Services/MailService.cs | 58 +++++++++++------ 8 files changed, 170 insertions(+), 53 deletions(-) diff --git a/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs b/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs index b64e1b2a..3e970d65 100644 --- a/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs +++ b/Wino.Core.Domain/MenuItems/MergedAccountMenuItem.cs @@ -20,14 +20,14 @@ public partial class MergedAccountMenuItem : MenuItemBase [ObservableProperty] - [NotifyPropertyChangedFor(nameof(SynchronizationProgress))] + [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))] public partial int TotalItemsToSync { get; set; } /// /// Remaining items to sync across all merged accounts. /// [ObservableProperty] - [NotifyPropertyChangedFor(nameof(SynchronizationProgress))] + [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))] public partial int RemainingItemsToSync { get; set; } /// @@ -50,6 +50,17 @@ public partial class MergedAccountMenuItem : MenuItemBase + /// Whether synchronization progress should be visible. + /// Visible when there's active synchronization (TotalItemsToSync > 0 or RemainingItemsToSync > 0). + /// + public bool IsSynchronizationProgressVisible => TotalItemsToSync > 0 || RemainingItemsToSync > 0; + + /// + /// Whether progress should be indeterminate. + /// + public bool IsProgressIndeterminate => TotalItemsToSync == 0 && IsSynchronizationProgressVisible; + [ObservableProperty] private string mergedAccountName; diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 7356c994..4302fc67 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -154,6 +154,7 @@ public class GmailSynchronizer : WinoSynchronizer()); @@ -220,12 +228,15 @@ public class GmailSynchronizer : WinoSynchronizer 0) + { + var remainingItems = totalInQueue - processedCount; + UpdateSyncProgress(totalInQueue, remainingItems, $"Processing emails: {processedCount}/{totalInQueue}"); + } + // If too many failures, pause to avoid hitting rate limits if (totalFailed > 85) await Task.Delay(TimeSpan.FromSeconds(10)); } diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index 4cad0aa3..fb80b461 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -514,9 +514,10 @@ public class WinoMailCollection : ObservableRecipient, IRecipient 0) { - var groupedItems = itemsToAdd + // Pre-compute grouping on background thread to reduce UI thread work + var groupedItems = await Task.Run(() => itemsToAdd .GroupBy(GetGroupingKey) - .ToDictionary(g => g.Key, g => g.ToList()); + .ToDictionary(g => g.Key, g => g.ToList())).ConfigureAwait(false); await ExecuteUIThread(() => { diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index 97dc067b..c301d7e9 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -571,7 +571,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, return; } - var viewModels = PrepareMailViewModels(items); + var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false); await MailCollection.AddRangeAsync(viewModels, false); await ExecuteUIThread(() => { IsInitializingFolder = false; }); @@ -738,9 +738,19 @@ public partial class MailListPageViewModel : MailBaseViewModel, } } - private List PrepareMailViewModels(IEnumerable mailItems) + private async Task> PrepareMailViewModelsAsync(IEnumerable mailItems, CancellationToken cancellationToken = default) { - return mailItems.Select(a => new MailItemViewModel(a)).ToList(); + // Run ViewModel creation on background thread to avoid blocking UI + return await Task.Run(() => + { + var viewModels = new List(); + foreach (var mailItem in mailItems) + { + cancellationToken.ThrowIfCancellationRequested(); + viewModels.Add(new MailItemViewModel(mailItem)); + } + return viewModels; + }, cancellationToken).ConfigureAwait(false); } [RelayCommand] @@ -764,7 +774,11 @@ public partial class MailListPageViewModel : MailBaseViewModel, if (ActiveFolder == null) return; - await ExecuteUIThread(() => { IsInitializingFolder = true; }); + await ExecuteUIThread(() => { + IsInitializingFolder = true; + // Show initial loading progress + UpdateBarMessage(InfoBarMessageType.Information, ActiveFolder.FolderName, "Loading emails..."); + }); // Folder is changed during initialization. // Just cancel the existing one and wait for new initialization. @@ -855,11 +869,21 @@ public partial class MailListPageViewModel : MailBaseViewModel, if (!listManipulationCancellationTokenSource.IsCancellationRequested) { + // Update progress: Creating view models + await ExecuteUIThread(() => { + UpdateBarMessage(InfoBarMessageType.Information, ActiveFolder.FolderName, $"Processing {items.Count} emails..."); + }); + // Here they are already threaded if needed. // We don't need to insert them one by one. // Just create VMs and do bulk insert. - var viewModels = PrepareMailViewModels(items); + var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false); + + // Update progress: Adding to collection + await ExecuteUIThread(() => { + UpdateBarMessage(InfoBarMessageType.Information, ActiveFolder.FolderName, "Finalizing..."); + }); await MailCollection.AddRangeAsync(viewModels, clearIdCache: true); @@ -895,6 +919,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, OnPropertyChanged(nameof(CanSynchronize)); NotifyItemFoundState(); + + // Clear the loading message after completion + IsBarOpen = false; }); } } diff --git a/Wino.Mail.WinUI/AppShell.xaml b/Wino.Mail.WinUI/AppShell.xaml index 16e2acc6..a00725ae 100644 --- a/Wino.Mail.WinUI/AppShell.xaml +++ b/Wino.Mail.WinUI/AppShell.xaml @@ -76,12 +76,25 @@ MaxLines="1" Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind Parameter.Address, Mode=OneWay}" - TextTrimming="CharacterEllipsis" /> + TextTrimming="CharacterEllipsis" + Visibility="{x:Bind IsSynchronizationProgressVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" /> + + + IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}" + Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" /> @@ -251,6 +261,10 @@ + + + + - + Text="{x:Bind SynchronizationStatus, Mode=OneWay}" + TextTrimming="CharacterEllipsis" + Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" /> + + + + + diff --git a/Wino.Mail.WinUI/Views/Settings/ContactsPage.xaml b/Wino.Mail.WinUI/Views/Settings/ContactsPage.xaml index d8da14e1..c5311211 100644 --- a/Wino.Mail.WinUI/Views/Settings/ContactsPage.xaml +++ b/Wino.Mail.WinUI/Views/Settings/ContactsPage.xaml @@ -95,7 +95,7 @@ - + diff --git a/Wino.Services/DatabaseService.cs b/Wino.Services/DatabaseService.cs index f23dab5e..15fe46d3 100644 --- a/Wino.Services/DatabaseService.cs +++ b/Wino.Services/DatabaseService.cs @@ -1,7 +1,6 @@ using System.IO; using System.Threading.Tasks; using SQLite; -using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; @@ -44,6 +43,10 @@ public class DatabaseService : IDatabaseService private async Task CreateTablesAsync() { + //typeof(AccountCalendar), + // typeof(CalendarEventAttendee), + // typeof(CalendarItem), + // typeof(Reminder), await Connection.CreateTablesAsync(CreateFlags.None, typeof(MailCopy), typeof(MailItemFolder), @@ -54,10 +57,6 @@ public class DatabaseService : IDatabaseService typeof(MergedInbox), typeof(MailAccountPreferences), typeof(MailAccountAlias), - typeof(AccountCalendar), - typeof(CalendarEventAttendee), - typeof(CalendarItem), - typeof(Reminder), typeof(Thumbnail), typeof(KeyboardShortcut), typeof(MailItemQueue) diff --git a/Wino.Services/MailService.cs b/Wino.Services/MailService.cs index fa2cea77..5140e28f 100644 --- a/Wino.Services/MailService.cs +++ b/Wino.Services/MailService.cs @@ -252,33 +252,35 @@ public class MailService : BaseDatabaseService, IMailService return [.. mails]; } - // Include other mails in the same threads + // Include other mails in the same threads - batch process to reduce DB calls var expandedMails = new List(mails); - var processedThreadIds = new HashSet(); + var uniqueThreadIds = mails + .Where(m => !string.IsNullOrEmpty(m.ThreadId)) + .Select(m => m.ThreadId) + .Distinct() + .ToList(); - foreach (var mail in mails) + if (uniqueThreadIds.Count > 0) { - if (!string.IsNullOrEmpty(mail.ThreadId) && !processedThreadIds.Contains(mail.ThreadId)) + // Get all thread mails in a single DB call + var existingMailIds = expandedMails.Select(m => m.Id).ToHashSet(); + var allThreadMails = await GetMailsByThreadIdsAsync(uniqueThreadIds, existingMailIds).ConfigureAwait(false); + + if (allThreadMails?.Count > 0) { - processedThreadIds.Add(mail.ThreadId); - - // Get all other mails with the same ThreadId that are not already in the result - var existingMailIds = expandedMails.Select(m => m.Id).ToHashSet(); - var threadMails = await GetMailsByThreadIdAsync(mail.ThreadId, existingMailIds).ConfigureAwait(false); - - if (threadMails?.Any() == true) + // Process thread mails in parallel to improve performance + var tasks = allThreadMails.Select(async threadMail => { - // Load assigned properties for the thread mails - foreach (var threadMail in threadMails) - { - await LoadAssignedPropertiesWithCacheAsync(threadMail, folderCache, accountCache, contactCache).ConfigureAwait(false); - } + await LoadAssignedPropertiesWithCacheAsync(threadMail, folderCache, accountCache, contactCache).ConfigureAwait(false); + return threadMail; + }); - // Remove items that have no assigned account or folder - threadMails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null); - - expandedMails.AddRange(threadMails); - } + var processedThreadMails = await Task.WhenAll(tasks).ConfigureAwait(false); + + // Filter out items with no assigned account or folder + var validThreadMails = processedThreadMails.Where(m => m.AssignedAccount != null && m.AssignedFolder != null); + + expandedMails.AddRange(validThreadMails); } cancellationToken.ThrowIfCancellationRequested(); @@ -301,6 +303,20 @@ public class MailService : BaseDatabaseService, IMailService return await Connection.QueryAsync(query); } + private async Task> GetMailsByThreadIdsAsync(List threadIds, HashSet excludeMailIds) + { + if (threadIds?.Count == 0) + return []; + + var query = new Query("MailCopy") + .WhereIn("ThreadId", threadIds) + .WhereNotIn("Id", excludeMailIds) + .SelectRaw("MailCopy.*") + .GetRawQuery(); + + return await Connection.QueryAsync(query).ConfigureAwait(false); + } + /// /// This method should used for operations with multiple mailItems. Don't use this for single mail items. /// Called method should provide own instances for caches.