folder structure fixes
This commit is contained in:
+10
-8
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# AGENTS.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to AI agent when working with code in this repository.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
@@ -12,14 +12,14 @@ Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replac
|
|||||||
# Open solution
|
# Open solution
|
||||||
# WinoMail.slnx is the main solution file (VS 2022+)
|
# WinoMail.slnx is the main solution file (VS 2022+)
|
||||||
|
|
||||||
# Build from command line
|
# Build WinUI project (Debug x64)
|
||||||
dotnet build WinoMail.slnx -c Debug
|
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
|
||||||
|
|
||||||
# Run tests
|
# Run tests (Debug x64)
|
||||||
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj
|
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64
|
||||||
|
|
||||||
# Build specific platform
|
# Copilot CLI build command (Debug x64)
|
||||||
dotnet build WinoMail.slnx -c Debug /p:Platform=x64
|
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
|
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
|
||||||
@@ -130,3 +130,5 @@ private string searchQuery = string.Empty;
|
|||||||
- Wrap async operations in try-catch
|
- Wrap async operations in try-catch
|
||||||
- Log errors via IWinoLogger
|
- Log errors via IWinoLogger
|
||||||
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
|
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +96,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
// Keeping a reference for quick access to the virtual archive folder.
|
// Keeping a reference for quick access to the virtual archive folder.
|
||||||
private Guid? archiveFolderId;
|
private Guid? archiveFolderId;
|
||||||
|
private bool _isFolderStructureChanged;
|
||||||
|
|
||||||
public GmailSynchronizer(MailAccount account,
|
public GmailSynchronizer(MailAccount account,
|
||||||
IGmailAuthenticator authenticator,
|
IGmailAuthenticator authenticator,
|
||||||
@@ -161,6 +162,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_isFolderStructureChanged = false;
|
||||||
|
|
||||||
// Make sure that virtual archive folder exists before all.
|
// Make sure that virtual archive folder exists before all.
|
||||||
if (!archiveFolderId.HasValue)
|
if (!archiveFolderId.HasValue)
|
||||||
await InitializeArchiveFolderAsync().ConfigureAwait(false);
|
await InitializeArchiveFolderAsync().ConfigureAwait(false);
|
||||||
@@ -178,6 +181,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
throw new GmailServiceDisabledException();
|
throw new GmailServiceDisabledException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isFolderStructureChanged)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
||||||
|
}
|
||||||
|
|
||||||
_logger.Information("Synchronizing folders for {Name} is completed", Account.Name);
|
_logger.Information("Synchronizing folders for {Name} is completed", Account.Name);
|
||||||
UpdateSyncProgress(0, 0, "Folders synchronized");
|
UpdateSyncProgress(0, 0, "Folders synchronized");
|
||||||
|
|
||||||
@@ -696,6 +704,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
};
|
};
|
||||||
|
|
||||||
await _gmailChangeProcessor.InsertFolderAsync(archiveFolder).ConfigureAwait(false);
|
await _gmailChangeProcessor.InsertFolderAsync(archiveFolder).ConfigureAwait(false);
|
||||||
|
_isFolderStructureChanged = true;
|
||||||
|
|
||||||
// Migration-> User might've already have another special folder for Archive.
|
// Migration-> User might've already have another special folder for Archive.
|
||||||
// We must remove that type assignment.
|
// We must remove that type assignment.
|
||||||
@@ -703,6 +712,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
var otherArchiveFolders = localFolders.Where(a => a.SpecialFolderType == SpecialFolderType.Archive && a.Id != archiveFolderId.Value).ToList();
|
var otherArchiveFolders = localFolders.Where(a => a.SpecialFolderType == SpecialFolderType.Archive && a.Id != archiveFolderId.Value).ToList();
|
||||||
|
|
||||||
|
if (otherArchiveFolders.Any())
|
||||||
|
{
|
||||||
|
_isFolderStructureChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var otherArchiveFolder in otherArchiveFolders)
|
foreach (var otherArchiveFolder in otherArchiveFolders)
|
||||||
{
|
{
|
||||||
otherArchiveFolder.SpecialFolderType = SpecialFolderType.Other;
|
otherArchiveFolder.SpecialFolderType = SpecialFolderType.Other;
|
||||||
@@ -803,7 +817,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
if (insertedFolders.Any() || deletedFolders.Any() || updatedFolders.Any())
|
if (insertedFolders.Any() || deletedFolders.Any() || updatedFolders.Any())
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
_isFolderStructureChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
private bool _isCalDavDiscoveryAttempted;
|
private bool _isCalDavDiscoveryAttempted;
|
||||||
private readonly IImapCalendarOperationHandler _localCalendarOperationHandler;
|
private readonly IImapCalendarOperationHandler _localCalendarOperationHandler;
|
||||||
private readonly IImapCalendarOperationHandler _calDavCalendarOperationHandler;
|
private readonly IImapCalendarOperationHandler _calDavCalendarOperationHandler;
|
||||||
|
private bool _isFolderStructureChanged;
|
||||||
|
|
||||||
public ImapSynchronizer(MailAccount account,
|
public ImapSynchronizer(MailAccount account,
|
||||||
IImapChangeProcessor imapChangeProcessor,
|
IImapChangeProcessor imapChangeProcessor,
|
||||||
@@ -557,6 +558,8 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_isFolderStructureChanged = false;
|
||||||
|
|
||||||
// Set indeterminate progress initially
|
// Set indeterminate progress initially
|
||||||
UpdateSyncProgress(0, 0, "Synchronizing...");
|
UpdateSyncProgress(0, 0, "Synchronizing...");
|
||||||
|
|
||||||
@@ -565,6 +568,11 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
if (shouldDoFolderSync)
|
if (shouldDoFolderSync)
|
||||||
{
|
{
|
||||||
await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false);
|
await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_isFolderStructureChanged)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Type != MailSynchronizationType.FoldersOnly)
|
if (options.Type != MailSynchronizationType.FoldersOnly)
|
||||||
@@ -1018,7 +1026,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
|
|
||||||
if (insertedFolders.Any() || deletedFolders.Any() || updatedFolders.Any())
|
if (insertedFolders.Any() || deletedFolders.Any() || updatedFolders.Any())
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
_isFolderStructureChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ using Wino.Core.Requests.Bundles;
|
|||||||
using Wino.Core.Requests.Calendar;
|
using Wino.Core.Requests.Calendar;
|
||||||
using Wino.Core.Requests.Folder;
|
using Wino.Core.Requests.Folder;
|
||||||
using Wino.Core.Requests.Mail;
|
using Wino.Core.Requests.Mail;
|
||||||
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Core.Synchronizers.Mail;
|
namespace Wino.Core.Synchronizers.Mail;
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
||||||
private readonly GraphServiceClient _graphClient;
|
private readonly GraphServiceClient _graphClient;
|
||||||
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
|
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
|
||||||
|
private bool _isFolderStructureChanged;
|
||||||
|
|
||||||
private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads
|
private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads
|
||||||
|
|
||||||
@@ -990,6 +992,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
if (IsResourceDeleted(folder.AdditionalData))
|
if (IsResourceDeleted(folder.AdditionalData))
|
||||||
{
|
{
|
||||||
await _outlookChangeProcessor.DeleteFolderAsync(Account.Id, folder.Id).ConfigureAwait(false);
|
await _outlookChangeProcessor.DeleteFolderAsync(Account.Id, folder.Id).ConfigureAwait(false);
|
||||||
|
_isFolderStructureChanged = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1022,6 +1025,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
item.ShowUnreadCount = item.SpecialFolderType != SpecialFolderType.Deleted || item.SpecialFolderType != SpecialFolderType.Other;
|
item.ShowUnreadCount = item.SpecialFolderType != SpecialFolderType.Deleted || item.SpecialFolderType != SpecialFolderType.Other;
|
||||||
|
|
||||||
await _outlookChangeProcessor.InsertFolderAsync(item).ConfigureAwait(false);
|
await _outlookChangeProcessor.InsertFolderAsync(item).ConfigureAwait(false);
|
||||||
|
_isFolderStructureChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1118,6 +1122,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
_isFolderStructureChanged = false;
|
||||||
|
|
||||||
var specialFolderInfo = await GetSpecialFolderIdsAsync(cancellationToken).ConfigureAwait(false);
|
var specialFolderInfo = await GetSpecialFolderIdsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var graphFolders = await GetDeltaFoldersAsync(cancellationToken).ConfigureAwait(false);
|
var graphFolders = await GetDeltaFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1128,6 +1134,11 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
await iterator.IterateAsync();
|
await iterator.IterateAsync();
|
||||||
|
|
||||||
await UpdateDeltaSynchronizationIdentifierAsync(iterator.Deltalink).ConfigureAwait(false);
|
await UpdateDeltaSynchronizationIdentifierAsync(iterator.Deltalink).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_isFolderStructureChanged)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T> DeserializeGraphBatchResponseAsync<T>(BatchResponseContentCollection collection, string requestId, CancellationToken cancellationToken = default) where T : IParsable, new()
|
private async Task<T> DeserializeGraphBatchResponseAsync<T>(BatchResponseContentCollection collection, string requestId, CancellationToken cancellationToken = default) where T : IParsable, new()
|
||||||
|
|||||||
@@ -969,6 +969,35 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsAccountCurrentlyLoaded(Guid accountId)
|
||||||
|
{
|
||||||
|
return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshLoadedAccountFolderStructureAsync(Guid accountId)
|
||||||
|
{
|
||||||
|
if (!IsAccountCurrentlyLoaded(accountId) || latestSelectedAccountMenuItem == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var selectedFolderId = (SelectedMenuItem as IBaseFolderMenuItem)?.HandlingFolders
|
||||||
|
?.FirstOrDefault(a => a.MailAccountId == accountId)?.Id;
|
||||||
|
|
||||||
|
var folders = await _folderService.GetAccountFoldersForDisplayAsync(latestSelectedAccountMenuItem);
|
||||||
|
|
||||||
|
await MenuItems.ReplaceFoldersAsync(folders);
|
||||||
|
await UpdateUnreadItemCountAsync();
|
||||||
|
|
||||||
|
if (selectedFolderId.HasValue &&
|
||||||
|
MenuItems.TryGetFolderMenuItem(selectedFolderId.Value, out IBaseFolderMenuItem selectedFolderMenuItem))
|
||||||
|
{
|
||||||
|
await NavigateFolderAsync(selectedFolderMenuItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await NavigateInboxAsync(latestSelectedAccountMenuItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async void Receive(RefreshUnreadCountsMessage message)
|
public async void Receive(RefreshUnreadCountsMessage message)
|
||||||
=> await UpdateUnreadItemCountAsync();
|
=> await UpdateUnreadItemCountAsync();
|
||||||
|
|
||||||
@@ -980,13 +1009,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
public async void Receive(AccountFolderConfigurationUpdated message)
|
public async void Receive(AccountFolderConfigurationUpdated message)
|
||||||
{
|
{
|
||||||
// Reloading of folders is needed to re-create folder tree if the account is loaded.
|
await RefreshLoadedAccountFolderStructureAsync(message.AccountId);
|
||||||
|
|
||||||
if (MenuItems.TryGetAccountMenuItem(message.AccountId, out IAccountMenuItem accountMenuItem) &&
|
|
||||||
latestSelectedAccountMenuItem == accountMenuItem)
|
|
||||||
{
|
|
||||||
await ChangeLoadedAccountAsync(accountMenuItem, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Receive(MergedInboxRenamed message)
|
public async void Receive(MergedInboxRenamed message)
|
||||||
@@ -1055,7 +1078,10 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
if (wasSelected && latestSelectedAccountMenuItem != null)
|
if (wasSelected && latestSelectedAccountMenuItem != null)
|
||||||
{
|
{
|
||||||
await NavigateInboxAsync(latestSelectedAccountMenuItem);
|
await NavigateInboxAsync(latestSelectedAccountMenuItem);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await RefreshLoadedAccountFolderStructureAsync(folder.MailAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder)
|
protected override void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder)
|
||||||
|
|||||||
Reference in New Issue
Block a user