Folder operations, Gmail folder sync improvements and rework of menu items. (#273)
* New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Disable vertical scroll for composing page editor items. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Disable vertical scroll for composing page editor items. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Disable vertical scroll for composing page editor items. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * Fixed an issue where redundant older updates causing pivots to be re-created. * New empty folder request * New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * New rename folder dialog keys. * New rename folder dialog keys. * Fixed an issue where redundant older updates causing pivots to be re-created. * New empty folder request * Enable empty folder on base sync. * Move updates on event listeners. * Remove folder UI messages. * Reworked folder synchronization for gmail. * Loading folders on the fly as the selected account changed instead of relying on cached menu items. * Merged account folder items, re-navigating to existing rendering page. * - Reworked merged account menu system. - Reworked unread item count loadings. - Fixed back button visibility. - Instant rendering of mails if renderer is active. - Animation fixes. - Menu item re-load crash/hang fixes. * Handle folder renaming on the UI. * Empty folder for all synchronizers. * New execution delay mechanism and handling folder mark as read for all synchronizers. * Revert UI changes on failure for IMAP. * Remove duplicate translation keys. * Cleanup.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.MenuItems;
|
||||
|
||||
@@ -7,31 +6,9 @@ namespace Wino.Core.Extensions
|
||||
{
|
||||
public static class FolderTreeExtensions
|
||||
{
|
||||
public static AccountMenuItem GetAccountMenuTree(this AccountFolderTree accountTree, IMenuItem parentMenuItem = null)
|
||||
{
|
||||
var accountMenuItem = new AccountMenuItem(accountTree.Account, parentMenuItem);
|
||||
|
||||
foreach (var structure in accountTree.Folders)
|
||||
{
|
||||
var tree = GetMenuItemByFolderRecursive(structure, accountMenuItem, null);
|
||||
|
||||
accountMenuItem.SubMenuItems.Add(tree);
|
||||
}
|
||||
|
||||
|
||||
// Create flat folder hierarchy for ease of access.
|
||||
accountMenuItem.FlattenedFolderHierarchy = ListExtensions
|
||||
.FlattenBy(accountMenuItem.SubMenuItems, a => a.SubMenuItems)
|
||||
.Where(a => a is FolderMenuItem)
|
||||
.Cast<FolderMenuItem>()
|
||||
.ToList();
|
||||
|
||||
return accountMenuItem;
|
||||
}
|
||||
|
||||
private static MenuItemBase<IMailItemFolder, FolderMenuItem> GetMenuItemByFolderRecursive(IMailItemFolder structure, AccountMenuItem parentAccountMenuItem, IMenuItem parentFolderItem)
|
||||
{
|
||||
MenuItemBase<IMailItemFolder, FolderMenuItem> parentMenuItem = new FolderMenuItem(structure, parentAccountMenuItem.Parameter, parentAccountMenuItem);
|
||||
MenuItemBase<IMailItemFolder, FolderMenuItem> parentMenuItem = new FolderMenuItem(structure, parentAccountMenuItem.Parameter, parentFolderItem);
|
||||
|
||||
var childStructures = structure.ChildFolders;
|
||||
|
||||
|
||||
@@ -18,57 +18,114 @@ namespace Wino.Core.Extensions
|
||||
public const string STARRED_LABEL_ID = "STARRED";
|
||||
public const string DRAFT_LABEL_ID = "DRAFT";
|
||||
public const string SENT_LABEL_ID = "SENT";
|
||||
public const string SPAM_LABEL_ID = "SPAM";
|
||||
public const string CHAT_LABEL_ID = "CHAT";
|
||||
public const string TRASH_LABEL_ID = "TRASH";
|
||||
|
||||
// Category labels.
|
||||
public const string FORUMS_LABEL_ID = "FORUMS";
|
||||
public const string UPDATES_LABEL_ID = "UPDATES";
|
||||
public const string PROMOTIONS_LABEL_ID = "PROMOTIONS";
|
||||
public const string SOCIAL_LABEL_ID = "SOCIAL";
|
||||
public const string PERSONAL_LABEL_ID = "PERSONAL";
|
||||
|
||||
// Label visibility identifiers.
|
||||
private const string SYSTEM_FOLDER_IDENTIFIER = "system";
|
||||
private const string FOLDER_HIDE_IDENTIFIER = "labelHide";
|
||||
|
||||
private static Dictionary<string, SpecialFolderType> KnownFolderDictioanry = new Dictionary<string, SpecialFolderType>()
|
||||
private const string CATEGORY_PREFIX = "CATEGORY_";
|
||||
private const string FOLDER_SEPERATOR_STRING = "/";
|
||||
private const char FOLDER_SEPERATOR_CHAR = '/';
|
||||
|
||||
private static Dictionary<string, SpecialFolderType> KnownFolderDictionary = new Dictionary<string, SpecialFolderType>()
|
||||
{
|
||||
{ INBOX_LABEL_ID, SpecialFolderType.Inbox },
|
||||
{ "CHAT", SpecialFolderType.Chat },
|
||||
{ CHAT_LABEL_ID, SpecialFolderType.Chat },
|
||||
{ IMPORTANT_LABEL_ID, SpecialFolderType.Important },
|
||||
{ "TRASH", SpecialFolderType.Deleted },
|
||||
{ TRASH_LABEL_ID, SpecialFolderType.Deleted },
|
||||
{ DRAFT_LABEL_ID, SpecialFolderType.Draft },
|
||||
{ SENT_LABEL_ID, SpecialFolderType.Sent },
|
||||
{ "SPAM", SpecialFolderType.Junk },
|
||||
{ SPAM_LABEL_ID, SpecialFolderType.Junk },
|
||||
{ STARRED_LABEL_ID, SpecialFolderType.Starred },
|
||||
{ UNREAD_LABEL_ID, SpecialFolderType.Unread },
|
||||
{ "FORUMS", SpecialFolderType.Forums },
|
||||
{ "UPDATES", SpecialFolderType.Updates },
|
||||
{ "PROMOTIONS", SpecialFolderType.Promotions },
|
||||
{ "SOCIAL", SpecialFolderType.Social},
|
||||
{ "PERSONAL", SpecialFolderType.Personal},
|
||||
{ FORUMS_LABEL_ID, SpecialFolderType.Forums },
|
||||
{ UPDATES_LABEL_ID, SpecialFolderType.Updates },
|
||||
{ PROMOTIONS_LABEL_ID, SpecialFolderType.Promotions },
|
||||
{ SOCIAL_LABEL_ID, SpecialFolderType.Social},
|
||||
{ PERSONAL_LABEL_ID, SpecialFolderType.Personal},
|
||||
};
|
||||
|
||||
public static MailItemFolder GetLocalFolder(this Label label, Guid accountId)
|
||||
public static string[] SubCategoryFolderLabelIds =
|
||||
[
|
||||
FORUMS_LABEL_ID,
|
||||
UPDATES_LABEL_ID,
|
||||
PROMOTIONS_LABEL_ID,
|
||||
SOCIAL_LABEL_ID,
|
||||
PERSONAL_LABEL_ID
|
||||
];
|
||||
|
||||
private static string GetNormalizedLabelName(string labelName)
|
||||
{
|
||||
var unchangedFolderName = label.Name;
|
||||
// 1. Remove CATEGORY_ prefix.
|
||||
var normalizedLabelName = labelName.Replace(CATEGORY_PREFIX, string.Empty);
|
||||
|
||||
if (label.Name.StartsWith("CATEGORY_"))
|
||||
label.Name = label.Name.Replace("CATEGORY_", "");
|
||||
// 2. Normalize label name by capitalizing first letter.
|
||||
normalizedLabelName = char.ToUpper(normalizedLabelName[0]) + normalizedLabelName.Substring(1).ToLower();
|
||||
|
||||
bool isSpecialFolder = KnownFolderDictioanry.ContainsKey(label.Name);
|
||||
return normalizedLabelName;
|
||||
}
|
||||
|
||||
public static MailItemFolder GetLocalFolder(this Label label, ListLabelsResponse labelsResponse, Guid accountId)
|
||||
{
|
||||
bool isAllCapital = label.Name.All(a => char.IsUpper(a));
|
||||
|
||||
var specialFolderType = isSpecialFolder ? KnownFolderDictioanry[label.Name] : SpecialFolderType.Other;
|
||||
var normalizedLabelName = GetFolderName(label);
|
||||
|
||||
return new MailItemFolder()
|
||||
// Even though we normalize the label name, check is done by capitalizing the label name.
|
||||
var capitalNormalizedLabelName = normalizedLabelName.ToUpper();
|
||||
|
||||
bool isSpecialFolder = KnownFolderDictionary.ContainsKey(capitalNormalizedLabelName);
|
||||
|
||||
var specialFolderType = isSpecialFolder ? KnownFolderDictionary[capitalNormalizedLabelName] : SpecialFolderType.Other;
|
||||
|
||||
// We used to support FOLDER_HIDE_IDENTIFIER to hide invisible folders.
|
||||
// However, a lot of people complained that they don't see their folders after the initial sync
|
||||
// without realizing that they are hidden in Gmail settings. Therefore, it makes more sense to ignore Gmail's configuration
|
||||
// since Wino allows folder visibility configuration separately.
|
||||
|
||||
// Overridden hidden labels are shown in the UI, but they have their synchronization disabled.
|
||||
// This is mainly because 'All Mails' label is hidden by default in Gmail, but there is no point to download all mails.
|
||||
|
||||
bool shouldEnableSynchronization = label.LabelListVisibility != FOLDER_HIDE_IDENTIFIER;
|
||||
bool isHidden = false;
|
||||
|
||||
bool isChildOfCategoryFolder = label.Name.StartsWith(CATEGORY_PREFIX);
|
||||
bool isSticky = isSpecialFolder && specialFolderType != SpecialFolderType.Category && !isChildOfCategoryFolder;
|
||||
|
||||
// By default, all special folders update unread count in the UI except Trash.
|
||||
bool shouldShowUnreadCount = specialFolderType != SpecialFolderType.Deleted || specialFolderType != SpecialFolderType.Other;
|
||||
|
||||
bool isSystemFolder = label.Type == SYSTEM_FOLDER_IDENTIFIER;
|
||||
|
||||
var localFolder = new MailItemFolder()
|
||||
{
|
||||
TextColorHex = label.Color?.TextColor,
|
||||
BackgroundColorHex = label.Color?.BackgroundColor,
|
||||
FolderName = isAllCapital ? char.ToUpper(label.Name[0]) + label.Name.Substring(1).ToLower() : label.Name, // Capitilize only first letter.
|
||||
FolderName = normalizedLabelName,
|
||||
RemoteFolderId = label.Id,
|
||||
Id = Guid.NewGuid(),
|
||||
MailAccountId = accountId,
|
||||
IsSynchronizationEnabled = true,
|
||||
IsSynchronizationEnabled = shouldEnableSynchronization,
|
||||
SpecialFolderType = specialFolderType,
|
||||
IsSystemFolder = label.Type == SYSTEM_FOLDER_IDENTIFIER,
|
||||
IsSticky = isSpecialFolder && specialFolderType != SpecialFolderType.Category && !unchangedFolderName.StartsWith("CATEGORY"),
|
||||
IsHidden = label.LabelListVisibility == FOLDER_HIDE_IDENTIFIER,
|
||||
|
||||
// By default, all special folders update unread count in the UI except Trash.
|
||||
ShowUnreadCount = specialFolderType != SpecialFolderType.Deleted || specialFolderType != SpecialFolderType.Other
|
||||
IsSystemFolder = isSystemFolder,
|
||||
IsSticky = isSticky,
|
||||
IsHidden = isHidden,
|
||||
ShowUnreadCount = shouldShowUnreadCount,
|
||||
};
|
||||
|
||||
localFolder.ParentRemoteFolderId = isChildOfCategoryFolder ? string.Empty : GetParentFolderRemoteId(label.Name, labelsResponse);
|
||||
|
||||
return localFolder;
|
||||
}
|
||||
|
||||
public static bool GetIsDraft(this Message message)
|
||||
@@ -83,6 +140,36 @@ namespace Wino.Core.Extensions
|
||||
public static bool GetIsFlagged(this Message message)
|
||||
=> message?.LabelIds?.Any(a => a == STARRED_LABEL_ID) ?? false;
|
||||
|
||||
private static string GetParentFolderRemoteId(string fullLabelName, ListLabelsResponse labelsResponse)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullLabelName)) return string.Empty;
|
||||
|
||||
// Find the last index of '/'
|
||||
int lastIndex = fullLabelName.LastIndexOf('/');
|
||||
|
||||
// If '/' not found or it's at the start, return the empty string.
|
||||
if (lastIndex <= 0) return string.Empty;
|
||||
|
||||
// Extract the parent label
|
||||
var parentLabelName = fullLabelName.Substring(0, lastIndex);
|
||||
|
||||
return labelsResponse.Labels.FirstOrDefault(a => a.Name == parentLabelName)?.Id ?? string.Empty;
|
||||
}
|
||||
|
||||
public static string GetFolderName(Label label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label.Name)) return string.Empty;
|
||||
|
||||
// Folders with "//" at the end has "/" as the name.
|
||||
if (label.Name.EndsWith(FOLDER_SEPERATOR_STRING)) return FOLDER_SEPERATOR_STRING;
|
||||
|
||||
string[] parts = label.Name.Split(FOLDER_SEPERATOR_CHAR);
|
||||
|
||||
var lastPart = parts[parts.Length - 1];
|
||||
|
||||
return GetNormalizedLabelName(lastPart);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns MailCopy out of native Gmail message and converted MimeMessage of that native messaage.
|
||||
/// </summary>
|
||||
@@ -157,6 +244,5 @@ namespace Wino.Core.Extensions
|
||||
|
||||
return new Tuple<MailCopy, MimeMessage, IEnumerable<string>>(mailCopy, mimeMessage, message.LabelIds);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,13 @@ namespace Wino.Core.Integration
|
||||
yield return new HttpRequestBundle<TNativeRequestType, TResponse>(action(item), batchChangeRequest);
|
||||
}
|
||||
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateHttpBundleWithResponse<TResponse>(
|
||||
IRequestBase item,
|
||||
Func<IRequestBase, TNativeRequestType> action)
|
||||
{
|
||||
yield return new HttpRequestBundle<TNativeRequestType, TResponse>(action(item), item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a batched HttpBundle with TResponse of expected response type from the http call for each of the items in the batch.
|
||||
/// Func will be executed for each item separately in the batch request.
|
||||
|
||||
@@ -28,12 +28,23 @@ namespace Wino.Core.Integration.Processors
|
||||
Task DeleteMailAsync(Guid accountId, string mailId);
|
||||
Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds);
|
||||
Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
|
||||
Task UpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders);
|
||||
Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
|
||||
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
|
||||
Task InsertFolderAsync(MailItemFolder folder);
|
||||
Task UpdateFolderAsync(MailItemFolder folder);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of folders that are available for account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account id to get folders for.</param>
|
||||
/// <returns>All folders.</returns>
|
||||
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
|
||||
|
||||
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
|
||||
|
||||
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
|
||||
Task UpdateFolderLastSyncDateAsync(Guid folderId);
|
||||
|
||||
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
|
||||
}
|
||||
|
||||
public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
||||
@@ -68,19 +79,6 @@ namespace Wino.Core.Integration.Processors
|
||||
/// </summary>
|
||||
/// <param name="folderId">Folder id to retrieve uIds for.</param>
|
||||
Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of folders that are available for account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account id to get folders for.</param>
|
||||
/// <returns>All folders.</returns>
|
||||
Task<List<MailItemFolder>> GetLocalIMAPFoldersAsync(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates folder.
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder to update.</param>
|
||||
Task UpdateFolderAsync(MailItemFolder folder);
|
||||
}
|
||||
|
||||
public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
@@ -116,14 +114,14 @@ namespace Wino.Core.Integration.Processors
|
||||
public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
|
||||
=> MailService.CreateMailAsync(accountId, package);
|
||||
|
||||
// Folder methods
|
||||
public Task UpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders)
|
||||
=> FolderService.BulkUpdateFolderStructureAsync(accountId, allFolders);
|
||||
public Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId)
|
||||
=> FolderService.GetFoldersAsync(accountId);
|
||||
|
||||
public Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
|
||||
=> MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
|
||||
|
||||
|
||||
public Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId)
|
||||
=> FolderService.GetFoldersAsync(accountId);
|
||||
|
||||
public Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options)
|
||||
=> FolderService.GetSynchronizationFoldersAsync(options);
|
||||
@@ -134,6 +132,9 @@ namespace Wino.Core.Integration.Processors
|
||||
public Task InsertFolderAsync(MailItemFolder folder)
|
||||
=> FolderService.InsertFolderAsync(folder);
|
||||
|
||||
public Task UpdateFolderAsync(MailItemFolder folder)
|
||||
=> FolderService.UpdateFolderAsync(folder);
|
||||
|
||||
public Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds)
|
||||
=> MailService.GetDownloadedUnreadMailsAsync(accountId, downloadedMailCopyIds);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Services;
|
||||
|
||||
@@ -17,13 +16,6 @@ namespace Wino.Core.Integration.Processors
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId)
|
||||
=> FolderService.GetKnownUidsForFolderAsync(folderId);
|
||||
|
||||
public Task<List<MailItemFolder>> GetLocalIMAPFoldersAsync(Guid accountId)
|
||||
=> FolderService.GetFoldersAsync(accountId);
|
||||
|
||||
public Task UpdateFolderAsync(MailItemFolder folder)
|
||||
=> FolderService.UpdateFolderAsync(folder);
|
||||
public Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId) => FolderService.GetKnownUidsForFolderAsync(folderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace Wino.Core.MenuItems
|
||||
{
|
||||
public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IMailItemFolder, FolderMenuItem>>, IAccountMenuItem
|
||||
{
|
||||
public List<FolderMenuItem> FlattenedFolderHierarchy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private int unreadItemCount;
|
||||
|
||||
@@ -20,6 +18,9 @@ namespace Wino.Core.MenuItems
|
||||
[NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible))]
|
||||
private double synchronizationProgress;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isEnabled = true;
|
||||
|
||||
public bool IsAttentionRequired => AttentionReason != AccountAttentionReason.None;
|
||||
public bool IsSynchronizationProgressVisible => SynchronizationProgress > 0 && SynchronizationProgress < 100;
|
||||
public Guid AccountId => Parameter.Id;
|
||||
@@ -88,8 +89,5 @@ namespace Wino.Core.MenuItems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetUnreadItemCountByFolderType(SpecialFolderType specialFolderType)
|
||||
=> FlattenedFolderHierarchy?.Where(a => a.SpecialFolderType == specialFolderType).Sum(a => a.UnreadItemCount) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ namespace Wino.Core.MenuItems
|
||||
|
||||
public bool ShowUnreadCount => Parameter.ShowUnreadCount;
|
||||
|
||||
IEnumerable<IMenuItem> IBaseFolderMenuItem.SubMenuItems => SubMenuItems;
|
||||
|
||||
public FolderMenuItem(IMailItemFolder folderStructure, MailAccount parentAccount, IMenuItem parentMenuItem) : base(folderStructure, folderStructure.Id, parentMenuItem)
|
||||
{
|
||||
ParentAccount = parentAccount;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MoreLinq.Extensions;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
@@ -8,64 +10,63 @@ namespace Wino.Core.MenuItems
|
||||
{
|
||||
public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
||||
{
|
||||
public IEnumerable<IBaseFolderMenuItem> GetFolderItems(Guid folderId)
|
||||
// Which types to remove from the list when folders are changing due to selection of new account.
|
||||
// We don't clear the whole list since we want to keep the New Mail button and account menu items.
|
||||
private readonly Type[] _preservingTypesForFolderArea = [typeof(AccountMenuItem), typeof(NewMailMenuItem), typeof(MergedAccountMenuItem)];
|
||||
private readonly IDispatcher _dispatcher;
|
||||
|
||||
public MenuItemCollection(IDispatcher dispatcher)
|
||||
{
|
||||
var rootItems = this.OfType<AccountMenuItem>()
|
||||
.SelectMany(a => a.FlattenedFolderHierarchy)
|
||||
.Where(a => a.Parameter?.Id == folderId)
|
||||
.Cast<IBaseFolderMenuItem>();
|
||||
|
||||
// Accounts that are merged can't exist in the root items.
|
||||
// Therefore if the folder is found in root items, return it without searching inside merged accounts.
|
||||
|
||||
if (rootItems.Any()) return rootItems;
|
||||
|
||||
var mergedItems = this.OfType<MergedAccountMenuItem>()
|
||||
.SelectMany(a => a.SubMenuItems.OfType<MergedAccountFolderMenuItem>()
|
||||
.Where(a => a.Parameter.Any(b => b.Id == folderId)))
|
||||
.Cast<IBaseFolderMenuItem>();
|
||||
|
||||
// Folder is found in the MergedInbox shared folders.
|
||||
if (mergedItems.Any()) return mergedItems;
|
||||
|
||||
// Folder is not in any of the above. Looks inside the individual accounts in merged inbox account menu item.
|
||||
var mergedAccountItems = this.OfType<MergedAccountMenuItem>()
|
||||
.SelectMany(a => a.SubMenuItems.OfType<AccountMenuItem>()
|
||||
.SelectMany(a => a.FlattenedFolderHierarchy)
|
||||
.Where(a => a.Parameter?.Id == folderId))
|
||||
.Cast<IBaseFolderMenuItem>();
|
||||
|
||||
return mergedAccountItems;
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public IBaseFolderMenuItem GetFolderItem(Guid folderId) => GetFolderItems(folderId).FirstOrDefault();
|
||||
|
||||
public IAccountMenuItem GetAccountMenuItem(Guid accountId)
|
||||
public IEnumerable<IAccountMenuItem> GetAllAccountMenuItems()
|
||||
{
|
||||
if (accountId == null) return null;
|
||||
foreach (var item in this)
|
||||
{
|
||||
if (item is MergedAccountMenuItem mergedAccountMenuItem)
|
||||
{
|
||||
foreach (var singleItem in mergedAccountMenuItem.SubMenuItems.OfType<IAccountMenuItem>())
|
||||
{
|
||||
yield return singleItem;
|
||||
}
|
||||
|
||||
if (TryGetRootAccountMenuItem(accountId, out IAccountMenuItem rootAccountMenuItem)) return rootAccountMenuItem;
|
||||
|
||||
return null;
|
||||
yield return mergedAccountMenuItem;
|
||||
}
|
||||
else if (item is IAccountMenuItem accountMenuItem)
|
||||
yield return accountMenuItem;
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern: Look for root account menu item only. Don't search inside the merged account menu item.
|
||||
public bool TryGetRootAccountMenuItem(Guid accountId, out IAccountMenuItem value)
|
||||
public IEnumerable<IBaseFolderMenuItem> GetAllFolderMenuItems(Guid folderId)
|
||||
{
|
||||
value = this.OfType<IAccountMenuItem>().FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId));
|
||||
foreach (var item in this)
|
||||
{
|
||||
if (item is IBaseFolderMenuItem folderMenuItem)
|
||||
{
|
||||
if (folderMenuItem.HandlingFolders.Any(a => a.Id == folderId))
|
||||
{
|
||||
yield return folderMenuItem;
|
||||
}
|
||||
else if (folderMenuItem.SubMenuItems.Any())
|
||||
{
|
||||
foreach (var subItem in folderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>())
|
||||
{
|
||||
if (subItem.HandlingFolders.Any(a => a.Id == folderId))
|
||||
{
|
||||
yield return subItem;
|
||||
}
|
||||
}
|
||||
|
||||
value ??= this.OfType<MergedAccountMenuItem>().FirstOrDefault(a => a.EntityId == accountId);
|
||||
|
||||
return value != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern: Look for root account menu item only and return the folder menu item inside the account menu item that has specific special folder type.
|
||||
public bool TryGetRootSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
||||
public bool TryGetAccountMenuItem(Guid accountId, out IAccountMenuItem value)
|
||||
{
|
||||
value = this.OfType<AccountMenuItem>()
|
||||
.Where(a => a.HoldingAccounts.Any(b => b.Id == accountId))
|
||||
.SelectMany(a => a.FlattenedFolderHierarchy)
|
||||
.FirstOrDefault(a => a.Parameter?.SpecialFolderType == specialFolderType);
|
||||
value = this.OfType<AccountMenuItem>().FirstOrDefault(a => a.AccountId == accountId);
|
||||
value ??= this.OfType<MergedAccountMenuItem>().FirstOrDefault(a => a.SubMenuItems.OfType<AccountMenuItem>().Where(b => b.AccountId == accountId) != null);
|
||||
|
||||
return value != null;
|
||||
}
|
||||
@@ -83,37 +84,49 @@ namespace Wino.Core.MenuItems
|
||||
// This will not look for the folders inside individual account menu items inside merged account menu item.
|
||||
public bool TryGetMergedAccountSpecialFolderMenuItem(Guid mergedInboxId, SpecialFolderType specialFolderType, out IBaseFolderMenuItem value)
|
||||
{
|
||||
value = this.OfType<MergedAccountMenuItem>()
|
||||
.Where(a => a.EntityId == mergedInboxId)
|
||||
.SelectMany(a => a.SubMenuItems)
|
||||
.OfType<MergedAccountFolderMenuItem>()
|
||||
value = this.OfType<MergedAccountFolderMenuItem>()
|
||||
.Where(a => a.MergedInbox.Id == mergedInboxId)
|
||||
.FirstOrDefault(a => a.SpecialFolderType == specialFolderType);
|
||||
|
||||
return value != null;
|
||||
}
|
||||
|
||||
// Pattern: Find the child account menu item inside the merged account menu item, locate the special folder menu item inside the child account menu item.
|
||||
public bool TryGetMergedAccountFolderMenuItemByAccountId(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
||||
public bool TryGetFolderMenuItem(Guid folderId, out IBaseFolderMenuItem value)
|
||||
{
|
||||
value = this.OfType<MergedAccountMenuItem>()
|
||||
.SelectMany(a => a.SubMenuItems)
|
||||
.OfType<AccountMenuItem>()
|
||||
.FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId))
|
||||
?.FlattenedFolderHierarchy
|
||||
.OfType<FolderMenuItem>()
|
||||
.FirstOrDefault(a => a.Parameter?.SpecialFolderType == specialFolderType);
|
||||
// Root folders
|
||||
value = this.OfType<IBaseFolderMenuItem>()
|
||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
||||
|
||||
value ??= this.OfType<FolderMenuItem>()
|
||||
.SelectMany(a => a.SubMenuItems)
|
||||
.OfType<IBaseFolderMenuItem>()
|
||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
||||
|
||||
return value != null;
|
||||
}
|
||||
|
||||
// Pattern: Find the common folder menu item with special folder type inside the merged account menu item for the given AccountId.
|
||||
public bool TryGetMergedAccountRootFolderMenuItemByAccountId(Guid accountId, SpecialFolderType specialFolderType, out MergedAccountFolderMenuItem value)
|
||||
public void UpdateUnreadItemCountsToZero()
|
||||
{
|
||||
value = this.OfType<MergedAccountMenuItem>()
|
||||
.Where(a => a.HoldingAccounts.Any(b => b.Id == accountId))
|
||||
.SelectMany(a => a.SubMenuItems)
|
||||
.OfType<MergedAccountFolderMenuItem>()
|
||||
.FirstOrDefault(a => a.SpecialFolderType == specialFolderType);
|
||||
// Handle the root folders.
|
||||
this.OfType<IBaseFolderMenuItem>().ForEach(a => RecursivelyResetUnreadItemCount(a));
|
||||
}
|
||||
|
||||
private void RecursivelyResetUnreadItemCount(IBaseFolderMenuItem baseFolderMenuItem)
|
||||
{
|
||||
baseFolderMenuItem.UnreadItemCount = 0;
|
||||
|
||||
if (baseFolderMenuItem.SubMenuItems == null) return;
|
||||
|
||||
foreach (var subMenuItem in baseFolderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>())
|
||||
{
|
||||
RecursivelyResetUnreadItemCount(subMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
||||
{
|
||||
value = this.OfType<IBaseFolderMenuItem>()
|
||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
|
||||
|
||||
return value != null;
|
||||
}
|
||||
@@ -138,12 +151,29 @@ namespace Wino.Core.MenuItems
|
||||
return accountMenuItem;
|
||||
}
|
||||
|
||||
public void ReplaceFolders(IEnumerable<IMenuItem> folders)
|
||||
public async Task ReplaceFoldersAsync(IEnumerable<IMenuItem> folders)
|
||||
{
|
||||
ClearFolderAreaMenuItems();
|
||||
await _dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
ClearFolderAreaMenuItems();
|
||||
|
||||
Items.Add(new SeperatorItem());
|
||||
AddRange(folders);
|
||||
Items.Add(new SeperatorItem());
|
||||
AddRange(folders);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables/disables account menu items in the list.
|
||||
/// </summary>
|
||||
/// <param name="isEnabled">Whether menu items should be enabled or disabled.</param>
|
||||
public async Task SetAccountMenuItemEnabledStatusAsync(bool isEnabled)
|
||||
{
|
||||
var accountItems = this.Where(a => a is IAccountMenuItem).Cast<IAccountMenuItem>();
|
||||
|
||||
await _dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
accountItems.ForEach(a => a.IsEnabled = isEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
public void AddAccountMenuItem(IAccountMenuItem accountMenuItem)
|
||||
@@ -158,18 +188,15 @@ namespace Wino.Core.MenuItems
|
||||
|
||||
private void ClearFolderAreaMenuItems()
|
||||
{
|
||||
var cloneItems = Items.ToList();
|
||||
var itemsToRemove = this.Where(a => !_preservingTypesForFolderArea.Contains(a.GetType())).ToList();
|
||||
|
||||
foreach (var item in cloneItems)
|
||||
itemsToRemove.ForEach(item =>
|
||||
{
|
||||
if (item is SeperatorItem || item is IBaseFolderMenuItem || item is MergedAccountMoreFolderMenuItem)
|
||||
{
|
||||
item.IsSelected = false;
|
||||
item.IsExpanded = false;
|
||||
item.IsExpanded = false;
|
||||
item.IsSelected = false;
|
||||
});
|
||||
|
||||
Remove(item);
|
||||
}
|
||||
}
|
||||
RemoveRange(itemsToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace Wino.Core.MenuItems
|
||||
|
||||
public bool ShowUnreadCount => HandlingFolders?.Any(a => a.ShowUnreadCount) ?? false;
|
||||
|
||||
public IEnumerable<IMenuItem> SubMenuItems => SubMenuItems;
|
||||
|
||||
[ObservableProperty]
|
||||
private int unreadItemCount;
|
||||
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.MenuItems
|
||||
{
|
||||
public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem>, IAccountMenuItem
|
||||
public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem>, IMergedAccountMenuItem
|
||||
{
|
||||
public int MergedAccountCount => GetAccountMenuItems().Count();
|
||||
public int MergedAccountCount => HoldingAccounts?.Count() ?? 0;
|
||||
|
||||
public IEnumerable<MailAccount> HoldingAccounts => GetAccountMenuItems()?.SelectMany(a => a.HoldingAccounts);
|
||||
public IEnumerable<MailAccount> HoldingAccounts { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private int unreadItemCount;
|
||||
@@ -22,34 +21,23 @@ namespace Wino.Core.MenuItems
|
||||
[ObservableProperty]
|
||||
private string mergedAccountName;
|
||||
|
||||
public MergedAccountMenuItem(MergedInbox mergedInbox, IMenuItem parent) : base(mergedInbox, mergedInbox.Id, parent)
|
||||
[ObservableProperty]
|
||||
private bool _isEnabled = true;
|
||||
|
||||
public MergedAccountMenuItem(MergedInbox mergedInbox, IEnumerable<MailAccount> holdingAccounts, IMenuItem parent) : base(mergedInbox, mergedInbox.Id, parent)
|
||||
{
|
||||
MergedAccountName = mergedInbox.Name;
|
||||
HoldingAccounts = holdingAccounts;
|
||||
}
|
||||
|
||||
public void RefreshFolderItemCount()
|
||||
{
|
||||
UnreadItemCount = GetAccountMenuItems().Select(a => a.GetUnreadItemCountByFolderType(SpecialFolderType.Inbox)).Sum();
|
||||
|
||||
var unreadUpdateFolders = SubMenuItems.OfType<IBaseFolderMenuItem>().Where(a => a.ShowUnreadCount);
|
||||
|
||||
foreach (var folder in unreadUpdateFolders)
|
||||
{
|
||||
folder.UnreadItemCount = GetAccountMenuItems().Select(a => a.GetUnreadItemCountByFolderType(folder.SpecialFolderType)).Sum();
|
||||
}
|
||||
}
|
||||
|
||||
// Accounts are always located in More folder of Merged Inbox menu item.
|
||||
public IEnumerable<AccountMenuItem> GetAccountMenuItems()
|
||||
{
|
||||
var moreFolder = SubMenuItems.OfType<MergedAccountMoreFolderMenuItem>().FirstOrDefault();
|
||||
|
||||
if (moreFolder == null) return default;
|
||||
|
||||
return moreFolder.SubMenuItems.OfType<AccountMenuItem>();
|
||||
UnreadItemCount = SubMenuItems.OfType<IAccountMenuItem>().Sum(a => a.UnreadItemCount);
|
||||
}
|
||||
|
||||
public void UpdateAccount(MailAccount account)
|
||||
=> GetAccountMenuItems().FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == account.Id))?.UpdateAccount(account);
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Messages.Mails
|
||||
{
|
||||
/// <summary>
|
||||
/// When rendering frame should be disposed.
|
||||
/// </summary>
|
||||
public class DisposeRenderingFrameRequested { }
|
||||
}
|
||||
31
Wino.Core/Requests/EmptyFolderRequest.cs
Normal file
31
Wino.Core/Requests/EmptyFolderRequest.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
|
||||
namespace Wino.Core.Requests
|
||||
{
|
||||
public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDelete) : FolderRequestBase(Folder, MailSynchronizerOperation.EmptyFolder), ICustomFolderSynchronizationRequest
|
||||
{
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
foreach (var item in MailsToDelete)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(item));
|
||||
}
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
foreach (var item in MailsToDelete)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new MailAddedMessage(item));
|
||||
}
|
||||
}
|
||||
|
||||
public List<Guid> SynchronizationFolderIds => [Folder.Id];
|
||||
}
|
||||
}
|
||||
37
Wino.Core/Requests/MarkFolderAsReadRequest.cs
Normal file
37
Wino.Core/Requests/MarkFolderAsReadRequest.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
|
||||
namespace Wino.Core.Requests
|
||||
{
|
||||
public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> MailsToMarkRead) : FolderRequestBase(Folder, MailSynchronizerOperation.MarkFolderRead), ICustomFolderSynchronizationRequest
|
||||
{
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
foreach (var item in MailsToMarkRead)
|
||||
{
|
||||
item.IsRead = true;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item));
|
||||
}
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
foreach (var item in MailsToMarkRead)
|
||||
{
|
||||
item.IsRead = false;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool DelayExecution => false;
|
||||
|
||||
public List<Guid> SynchronizationFolderIds => [Folder.Id];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
using Wino.Core.Domain.Entities;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
|
||||
namespace Wino.Core.Requests
|
||||
{
|
||||
public record RenameFolderRequest(MailItemFolder Folder) : FolderRequestBase(Folder, MailSynchronizerOperation.RenameFolder)
|
||||
public record RenameFolderRequest(MailItemFolder Folder, string CurrentFolderName, string NewFolderName) : FolderRequestBase(Folder, MailSynchronizerOperation.RenameFolder)
|
||||
{
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
|
||||
Folder.FolderName = NewFolderName;
|
||||
WeakReferenceMessenger.Default.Send(new FolderRenamed(Folder));
|
||||
}
|
||||
|
||||
public override void RevertUIChanges()
|
||||
{
|
||||
|
||||
Folder.FolderName = CurrentFolderName;
|
||||
WeakReferenceMessenger.Default.Send(new FolderRenamed(Folder));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,5 +55,7 @@ namespace Wino.Core.Requests
|
||||
{
|
||||
Items.ForEach(item => WeakReferenceMessenger.Default.Send(new MailAddedMessage(item.Item)));
|
||||
}
|
||||
|
||||
public override bool DelayExecution => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
|
||||
namespace Wino.Core.Requests
|
||||
@@ -9,10 +10,6 @@ namespace Wino.Core.Requests
|
||||
public record MailUpdatedMessage(MailCopy UpdatedMail) : IUIMessage;
|
||||
public record MailDownloadedMessage(MailCopy DownloadedMail) : IUIMessage;
|
||||
|
||||
public record FolderAddedMessage(MailItemFolder AddedFolder, MailAccount Account) : IUIMessage;
|
||||
public record FolderRemovedMessage(MailItemFolder RemovedFolder, MailAccount Account) : IUIMessage;
|
||||
public record FolderUpdatedMessage(MailItemFolder UpdatedFolder, MailAccount Account) : IUIMessage;
|
||||
|
||||
public record AccountCreatedMessage(MailAccount Account) : IUIMessage;
|
||||
public record AccountRemovedMessage(MailAccount Account) : IUIMessage;
|
||||
public record AccountUpdatedMessage(MailAccount Account) : IUIMessage;
|
||||
@@ -22,4 +19,7 @@ namespace Wino.Core.Requests
|
||||
public record DraftMapped(string LocalDraftCopyId, string RemoteDraftCopyId) : IUIMessage;
|
||||
|
||||
public record MergedInboxRenamed(Guid MergedInboxId, string NewName) : IUIMessage;
|
||||
|
||||
public record FolderRenamed(IMailItemFolder MailItemFolder) : IUIMessage;
|
||||
public record FolderSynchronizationEnabled(IMailItemFolder MailItemFolder) : IUIMessage;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using SqlKata;
|
||||
@@ -9,10 +10,12 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.MenuItems;
|
||||
using Wino.Core.Requests;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
@@ -168,6 +171,155 @@ namespace Wino.Core.Services
|
||||
return accountTree;
|
||||
}
|
||||
|
||||
|
||||
public Task<IEnumerable<IMenuItem>> GetAccountFoldersForDisplayAsync(IAccountMenuItem accountMenuItem)
|
||||
{
|
||||
if (accountMenuItem is IMergedAccountMenuItem mergedAccountFolderMenuItem)
|
||||
{
|
||||
return GetMergedAccountFolderMenuItemsAsync(mergedAccountFolderMenuItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetSingleAccountFolderMenuItemsAsync(accountMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<FolderMenuItem> GetPreparedFolderMenuItemRecursiveAsync(MailAccount account, MailItemFolder parentFolder, IMenuItem parentMenuItem)
|
||||
{
|
||||
// Localize category folder name.
|
||||
if (parentFolder.SpecialFolderType == SpecialFolderType.Category) parentFolder.FolderName = Translator.CategoriesFolderNameOverride;
|
||||
|
||||
var query = new Query(nameof(MailItemFolder))
|
||||
.Where(nameof(MailItemFolder.ParentRemoteFolderId), parentFolder.RemoteFolderId)
|
||||
.Where(nameof(MailItemFolder.MailAccountId), parentFolder.MailAccountId);
|
||||
|
||||
var preparedFolder = new FolderMenuItem(parentFolder, account, parentMenuItem);
|
||||
|
||||
var childFolders = await Connection.QueryAsync<MailItemFolder>(query.GetRawQuery()).ConfigureAwait(false);
|
||||
|
||||
if (childFolders.Any())
|
||||
{
|
||||
foreach (var subChildFolder in childFolders)
|
||||
{
|
||||
var preparedChild = await GetPreparedFolderMenuItemRecursiveAsync(account, subChildFolder, preparedFolder);
|
||||
|
||||
if (preparedChild == null) continue;
|
||||
|
||||
preparedFolder.SubMenuItems.Add(preparedChild);
|
||||
}
|
||||
}
|
||||
|
||||
return preparedFolder;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<IMenuItem>> GetSingleAccountFolderMenuItemsAsync(IAccountMenuItem accountMenuItem)
|
||||
{
|
||||
var accountId = accountMenuItem.EntityId.Value;
|
||||
var preparedFolderMenuItems = new List<IMenuItem>();
|
||||
|
||||
// Get all folders for the account. Excluding hidden folders.
|
||||
var folders = await GetVisibleFoldersAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
if (!folders.Any()) return new List<IMenuItem>();
|
||||
|
||||
var mailAccount = accountMenuItem.HoldingAccounts.First();
|
||||
|
||||
var listingFolders = folders.OrderBy(a => a.SpecialFolderType);
|
||||
|
||||
var moreFolder = MailItemFolder.CreateMoreFolder();
|
||||
var categoryFolder = MailItemFolder.CreateCategoriesFolder();
|
||||
|
||||
var moreFolderMenuItem = new FolderMenuItem(moreFolder, mailAccount, accountMenuItem);
|
||||
var categoryFolderMenuItem = new FolderMenuItem(categoryFolder, mailAccount, accountMenuItem);
|
||||
|
||||
foreach (var item in listingFolders)
|
||||
{
|
||||
// Category type folders should be skipped. They will be categorized under virtual category folder.
|
||||
if (GoogleIntegratorExtensions.SubCategoryFolderLabelIds.Contains(item.RemoteFolderId)) continue;
|
||||
|
||||
bool skipEmptyParentRemoteFolders = mailAccount.ProviderType == MailProviderType.Gmail;
|
||||
|
||||
if (skipEmptyParentRemoteFolders && !string.IsNullOrEmpty(item.ParentRemoteFolderId)) continue;
|
||||
|
||||
// Sticky items belong to account menu item directly. Rest goes to More folder.
|
||||
IMenuItem parentFolderMenuItem = item.IsSticky ? accountMenuItem : (GoogleIntegratorExtensions.SubCategoryFolderLabelIds.Contains(item.FolderName.ToUpper()) ? categoryFolderMenuItem : moreFolderMenuItem);
|
||||
|
||||
var preparedItem = await GetPreparedFolderMenuItemRecursiveAsync(mailAccount, item, parentFolderMenuItem).ConfigureAwait(false);
|
||||
|
||||
// Don't add menu items that are prepared for More folder. They've been included in More virtual folder already.
|
||||
// We'll add More folder later on at the end of the list.
|
||||
|
||||
if (preparedItem == null) continue;
|
||||
|
||||
if (item.IsSticky)
|
||||
{
|
||||
preparedFolderMenuItems.Add(preparedItem);
|
||||
}
|
||||
else if (parentFolderMenuItem is FolderMenuItem baseParentFolderMenuItem)
|
||||
{
|
||||
baseParentFolderMenuItem.SubMenuItems.Add(preparedItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Only add category folder if it's Gmail.
|
||||
if (mailAccount.ProviderType == MailProviderType.Gmail) preparedFolderMenuItems.Add(categoryFolderMenuItem);
|
||||
|
||||
// Only add More folder if there are any items in it.
|
||||
if (moreFolderMenuItem.SubMenuItems.Any()) preparedFolderMenuItems.Add(moreFolderMenuItem);
|
||||
|
||||
return preparedFolderMenuItems;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<IMenuItem>> GetMergedAccountFolderMenuItemsAsync(IMergedAccountMenuItem mergedAccountFolderMenuItem)
|
||||
{
|
||||
var holdingAccounts = mergedAccountFolderMenuItem.HoldingAccounts;
|
||||
|
||||
if (holdingAccounts == null || !holdingAccounts.Any()) return [];
|
||||
|
||||
var preparedFolderMenuItems = new List<IMenuItem>();
|
||||
|
||||
// First gather all account folders.
|
||||
// Prepare single menu items for both of them.
|
||||
|
||||
var allAccountFolders = new List<List<MailItemFolder>>();
|
||||
|
||||
foreach (var account in holdingAccounts)
|
||||
{
|
||||
var accountFolders = await GetVisibleFoldersAsync(account.Id).ConfigureAwait(false);
|
||||
|
||||
allAccountFolders.Add(accountFolders);
|
||||
}
|
||||
|
||||
var commonFolders = FindCommonFolders(allAccountFolders);
|
||||
|
||||
// Prepare menu items for common folders.
|
||||
foreach (var commonFolderType in commonFolders)
|
||||
{
|
||||
var folderItems = allAccountFolders.SelectMany(a => a.Where(b => b.SpecialFolderType == commonFolderType)).Cast<IMailItemFolder>().ToList();
|
||||
var menuItem = new MergedAccountFolderMenuItem(folderItems, null, mergedAccountFolderMenuItem.Parameter);
|
||||
|
||||
preparedFolderMenuItems.Add(menuItem);
|
||||
}
|
||||
|
||||
return preparedFolderMenuItems;
|
||||
}
|
||||
|
||||
private HashSet<SpecialFolderType> FindCommonFolders(List<List<MailItemFolder>> lists)
|
||||
{
|
||||
var allSpecialTypesExceptOther = Enum.GetValues(typeof(SpecialFolderType)).Cast<SpecialFolderType>().Where(a => a != SpecialFolderType.Other).ToList();
|
||||
|
||||
// Start with all special folder types from the first list
|
||||
var commonSpecialFolderTypes = new HashSet<SpecialFolderType>(allSpecialTypesExceptOther);
|
||||
|
||||
// Intersect with special folder types from all lists
|
||||
foreach (var list in lists)
|
||||
{
|
||||
commonSpecialFolderTypes.IntersectWith(list.Select(f => f.SpecialFolderType));
|
||||
}
|
||||
|
||||
return commonSpecialFolderTypes;
|
||||
}
|
||||
|
||||
private async Task<MailItemFolder> GetChildFolderItemsRecursiveAsync(Guid folderId, Guid accountId)
|
||||
{
|
||||
var folder = await Connection.Table<MailItemFolder>().Where(a => a.Id == folderId && a.MailAccountId == accountId).FirstOrDefaultAsync();
|
||||
@@ -198,49 +350,22 @@ namespace Wino.Core.Services
|
||||
=> Connection.Table<MailCopy>().Where(a => a.FolderId == folderId).CountAsync();
|
||||
|
||||
public Task<List<MailItemFolder>> GetFoldersAsync(Guid accountId)
|
||||
=> Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == accountId).ToListAsync();
|
||||
|
||||
public async Task UpdateCustomServerMailListAsync(Guid accountId, List<MailItemFolder> folders)
|
||||
{
|
||||
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
var query = new Query(nameof(MailItemFolder))
|
||||
.Where(nameof(MailItemFolder.MailAccountId), accountId)
|
||||
.OrderBy(nameof(MailItemFolder.SpecialFolderType));
|
||||
|
||||
if (account == null)
|
||||
return;
|
||||
return Connection.QueryAsync<MailItemFolder>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
// IMAP servers don't have unique identifier for folders all the time.
|
||||
// We'll map them with parent-name relation.
|
||||
public Task<List<MailItemFolder>> GetVisibleFoldersAsync(Guid accountId)
|
||||
{
|
||||
var query = new Query(nameof(MailItemFolder))
|
||||
.Where(nameof(MailItemFolder.MailAccountId), accountId)
|
||||
.Where(nameof(MailItemFolder.IsHidden), false)
|
||||
.OrderBy(nameof(MailItemFolder.SpecialFolderType));
|
||||
|
||||
var currentFolders = await GetFoldersAsync(accountId);
|
||||
|
||||
// These folders don't exist anymore. Remove them.
|
||||
var localRemoveFolders = currentFolders.ExceptBy(folders, a => a.RemoteFolderId);
|
||||
|
||||
foreach (var currentFolder in currentFolders)
|
||||
{
|
||||
// Check if we have this folder locally.
|
||||
var remotelyExistFolder = folders.FirstOrDefault(a => a.RemoteFolderId == currentFolder.RemoteFolderId
|
||||
&& a.ParentRemoteFolderId == currentFolder.ParentRemoteFolderId);
|
||||
|
||||
if (remotelyExistFolder == null)
|
||||
{
|
||||
// This folder is removed.
|
||||
// Remove everything for this folder.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var currentFolder = await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.RemoteFolderId == folder.RemoteFolderId);
|
||||
|
||||
// Nothing is changed, it's still the same folder.
|
||||
// Just update Id of the folder.
|
||||
|
||||
if (currentFolder != null)
|
||||
folder.Id = currentFolder.Id;
|
||||
|
||||
await Connection.InsertOrReplaceAsync(folder);
|
||||
}
|
||||
return Connection.QueryAsync<MailItemFolder>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public async Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId)
|
||||
@@ -301,6 +426,8 @@ namespace Wino.Core.Services
|
||||
localFolder.IsSynchronizationEnabled = isSynchronizationEnabled;
|
||||
|
||||
await UpdateFolderAsync(localFolder).ConfigureAwait(false);
|
||||
|
||||
Messenger.Send(new FolderSynchronizationEnabled(localFolder));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,11 +464,19 @@ namespace Wino.Core.Services
|
||||
_logger.Debug("Inserting folder {Id} - {FolderName}", folder.Id, folder.FolderName, folder.MailAccountId);
|
||||
|
||||
await Connection.InsertAsync(folder).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new FolderAddedMessage(folder, account));
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: This is not alright. We should've updated the folder instead of inserting.
|
||||
// Now we need to match the properties that user might've set locally.
|
||||
|
||||
folder.Id = existingFolder.Id;
|
||||
folder.IsSticky = existingFolder.IsSticky;
|
||||
folder.SpecialFolderType = existingFolder.SpecialFolderType;
|
||||
folder.ShowUnreadCount = existingFolder.ShowUnreadCount;
|
||||
folder.TextColorHex = existingFolder.TextColorHex;
|
||||
folder.BackgroundColorHex = existingFolder.BackgroundColorHex;
|
||||
|
||||
_logger.Debug("Folder {Id} - {FolderName} already exists. Updating.", folder.Id, folder.FolderName);
|
||||
|
||||
await UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
@@ -364,13 +499,9 @@ namespace Wino.Core.Services
|
||||
return;
|
||||
}
|
||||
|
||||
#if !DEBUG // Annoying
|
||||
_logger.Debug("Updating folder {FolderName}", folder.Id, folder.FolderName);
|
||||
#endif
|
||||
|
||||
await Connection.UpdateAsync(folder).ConfigureAwait(false);
|
||||
|
||||
ReportUIChange(new FolderUpdatedMessage(folder, account));
|
||||
}
|
||||
|
||||
private async Task DeleteFolderAsync(MailItemFolder folder)
|
||||
@@ -393,9 +524,10 @@ namespace Wino.Core.Services
|
||||
|
||||
await Connection.DeleteAsync(folder).ConfigureAwait(false);
|
||||
|
||||
// TODO: Delete all mail copies for this folder.
|
||||
// Delete all existing mails from this folder.
|
||||
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE FolderId = ?", folder.Id);
|
||||
|
||||
ReportUIChange(new FolderRemovedMessage(folder, account));
|
||||
// TODO: Delete MIME messages from the disk.
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -426,8 +558,6 @@ namespace Wino.Core.Services
|
||||
public Task<List<MailFolderPairMetadata>> GetMailFolderPairMetadatasAsync(string mailCopyId)
|
||||
=> GetMailFolderPairMetadatasAsync(new List<string>() { mailCopyId });
|
||||
|
||||
public async Task SetSpecialFolderAsync(Guid folderId, SpecialFolderType type)
|
||||
=> await Connection.ExecuteAsync("UPDATE MailItemFolder SET SpecialFolderType = ? WHERE Id = ?", type, folderId);
|
||||
|
||||
public async Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options)
|
||||
{
|
||||
@@ -489,38 +619,6 @@ namespace Wino.Core.Services
|
||||
public Task<MailItemFolder> GetFolderAsync(Guid accountId, string remoteFolderId)
|
||||
=> Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.RemoteFolderId == remoteFolderId);
|
||||
|
||||
// v2
|
||||
public async Task BulkUpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders)
|
||||
{
|
||||
var existingFolders = await GetFoldersAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
var foldersToInsert = allFolders.ExceptBy(existingFolders, a => a.RemoteFolderId);
|
||||
var foldersToDelete = existingFolders.ExceptBy(allFolders, a => a.RemoteFolderId);
|
||||
var foldersToUpdate = allFolders.Except(foldersToInsert).Except(foldersToDelete);
|
||||
|
||||
_logger.Debug("Found {0} folders to insert, {1} folders to update and {2} folders to delete.",
|
||||
foldersToInsert.Count(),
|
||||
foldersToUpdate.Count(),
|
||||
foldersToDelete.Count());
|
||||
|
||||
foreach (var folder in foldersToInsert)
|
||||
{
|
||||
await InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in foldersToUpdate)
|
||||
{
|
||||
await UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in foldersToDelete)
|
||||
{
|
||||
await DeleteFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task DeleteFolderAsync(Guid accountId, string remoteFolderId)
|
||||
{
|
||||
var folder = await GetFolderAsync(accountId, remoteFolderId);
|
||||
@@ -547,33 +645,6 @@ namespace Wino.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
// Inbox folder is always included for account menu item unread count.
|
||||
public Task<List<MailItemFolder>> GetUnreadUpdateFoldersAsync(Guid accountId)
|
||||
=> Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == accountId && (a.ShowUnreadCount || a.SpecialFolderType == SpecialFolderType.Inbox)).ToListAsync();
|
||||
|
||||
public async Task TestAsync()
|
||||
{
|
||||
var account = new MailAccount()
|
||||
{
|
||||
Address = "test@test.com",
|
||||
ProviderType = MailProviderType.Gmail,
|
||||
Name = "Test Account",
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
|
||||
await Connection.InsertAsync(account);
|
||||
|
||||
var pref = new MailAccountPreferences
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = account.Id
|
||||
};
|
||||
|
||||
await Connection.InsertAsync(pref);
|
||||
|
||||
ReportUIChange(new AccountCreatedMessage(account));
|
||||
}
|
||||
|
||||
public async Task<bool> IsInboxAvailableForAccountAsync(Guid accountId)
|
||||
=> (await Connection.Table<MailItemFolder>()
|
||||
.Where(a => a.SpecialFolderType == SpecialFolderType.Inbox && a.MailAccountId == accountId)
|
||||
@@ -581,5 +652,18 @@ namespace Wino.Core.Services
|
||||
|
||||
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
|
||||
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET LastSynchronizedDate = ? WHERE Id = ?", DateTime.UtcNow, folderId);
|
||||
|
||||
public Task<List<UnreadItemCountResult>> GetUnreadItemCountResultsAsync(IEnumerable<Guid> accountIds)
|
||||
{
|
||||
var query = new Query(nameof(MailCopy))
|
||||
.Join(nameof(MailItemFolder), $"{nameof(MailCopy)}.FolderId", $"{nameof(MailItemFolder)}.Id")
|
||||
.WhereIn($"{nameof(MailItemFolder)}.MailAccountId", accountIds)
|
||||
.Where($"{nameof(MailCopy)}.IsRead", 0)
|
||||
.Where($"{nameof(MailItemFolder)}.ShowUnreadCount", 1)
|
||||
.SelectRaw($"{nameof(MailItemFolder)}.Id as FolderId, {nameof(MailItemFolder)}.SpecialFolderType as SpecialFolderType, count (DISTINCT {nameof(MailCopy)}.Id) as UnreadItemCount, {nameof(MailItemFolder)}.MailAccountId as AccountId")
|
||||
.GroupBy($"{nameof(MailItemFolder)}.Id");
|
||||
|
||||
return Connection.QueryAsync<UnreadItemCountResult>(query.GetRawQuery());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Kiota.Abstractions.Extensions;
|
||||
using MimeKit;
|
||||
using MimeKit.Text;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using SqlKata;
|
||||
@@ -111,9 +110,29 @@ namespace Wino.Core.Services
|
||||
return copy;
|
||||
}
|
||||
|
||||
public Task<List<string>> GetMailIdsByFolderIdAsync(Guid folderId)
|
||||
=> Connection.QueryScalarsAsync<string>("SELECT Id FROM MailCopy WHERE FolderId = ?", folderId);
|
||||
public async Task<List<MailCopy>> GetMailsByFolderIdAsync(Guid folderId)
|
||||
{
|
||||
var mails = await Connection.QueryAsync<MailCopy>("SELECT * FROM MailCopy WHERE FolderId = ?", folderId);
|
||||
|
||||
foreach (var mail in mails)
|
||||
{
|
||||
await LoadAssignedPropertiesAsync(mail).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return mails;
|
||||
}
|
||||
|
||||
public async Task<List<MailCopy>> GetUnreadMailsByFolderIdAsync(Guid folderId)
|
||||
{
|
||||
var unreadMails = await Connection.QueryAsync<MailCopy>("SELECT * FROM MailCopy WHERE FolderId = ? AND IsRead = 0", folderId);
|
||||
|
||||
foreach (var mail in unreadMails)
|
||||
{
|
||||
await LoadAssignedPropertiesAsync(mail).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return unreadMails;
|
||||
}
|
||||
|
||||
private string BuildMailFetchQuery(MailListInitializationOptions options)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,6 @@ using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Messages.Synchronization;
|
||||
using Wino.Core.Requests;
|
||||
@@ -85,13 +84,17 @@ namespace Wino.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(FolderOperation operation, IMailItemFolder folderStructure)
|
||||
public async Task ExecuteAsync(FolderOperationPreperationRequest folderRequest)
|
||||
{
|
||||
IRequest request = null;
|
||||
if (folderRequest == null || folderRequest.Folder == null) return;
|
||||
|
||||
IRequestBase request = null;
|
||||
|
||||
var accountId = folderRequest.Folder.MailAccountId;
|
||||
|
||||
try
|
||||
{
|
||||
request = await _winoRequestProcessor.PrepareFolderRequestAsync(operation, folderStructure);
|
||||
request = await _winoRequestProcessor.PrepareFolderRequestAsync(folderRequest);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
@@ -102,7 +105,10 @@ namespace Wino.Core.Services
|
||||
Log.Error(ex, "Folder operation execution failed.");
|
||||
}
|
||||
|
||||
// _synchronizationWorker.Queue(request);
|
||||
if (request == null) return;
|
||||
|
||||
QueueRequest(request, accountId);
|
||||
QueueSynchronization(accountId);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest)
|
||||
@@ -125,7 +131,7 @@ namespace Wino.Core.Services
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void QueueRequest(IRequest request, Guid accountId)
|
||||
private void QueueRequest(IRequestBase request, Guid accountId)
|
||||
{
|
||||
var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(accountId);
|
||||
|
||||
|
||||
@@ -215,45 +215,45 @@ namespace Wino.Core.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IRequest> PrepareFolderRequestAsync(FolderOperation operation, IMailItemFolder mailItemFolder)
|
||||
public async Task<IRequestBase> PrepareFolderRequestAsync(FolderOperationPreperationRequest request)
|
||||
{
|
||||
if (mailItemFolder == null) return default;
|
||||
if (request == null || request.Folder == null) return default;
|
||||
|
||||
var accountId = mailItemFolder.MailAccountId;
|
||||
IRequestBase change = null;
|
||||
|
||||
IRequest change = null;
|
||||
var folder = request.Folder;
|
||||
var operation = request.Action;
|
||||
|
||||
switch (operation)
|
||||
switch (request.Action)
|
||||
{
|
||||
case FolderOperation.Pin:
|
||||
case FolderOperation.Unpin:
|
||||
await _folderService.ChangeStickyStatusAsync(mailItemFolder.Id, operation == FolderOperation.Pin);
|
||||
await _folderService.ChangeStickyStatusAsync(folder.Id, operation == FolderOperation.Pin);
|
||||
break;
|
||||
//case FolderOperation.MarkAllAsRead:
|
||||
// // Get all mails in the folder.
|
||||
|
||||
// var mailItems = await _folderService.GetAllUnreadItemsByFolderIdAsync(accountId, folderStructure.RemoteFolderId).ConfigureAwait(false);
|
||||
case FolderOperation.Rename:
|
||||
var newFolderName = await _dialogService.ShowTextInputDialogAsync(folder.FolderName, Translator.DialogMessage_RenameFolderTitle, Translator.DialogMessage_RenameFolderMessage, Translator.FolderOperation_Rename);
|
||||
|
||||
// if (mailItems.Any())
|
||||
// change = new FolderMarkAsReadRequest(accountId, mailItems.Select(a => a.Id).Distinct(), folderStructure.RemoteFolderId, folderStructure.FolderId);
|
||||
if (!string.IsNullOrEmpty(newFolderName))
|
||||
{
|
||||
change = new RenameFolderRequest(folder, folder.FolderName, newFolderName);
|
||||
}
|
||||
|
||||
// break;
|
||||
//case FolderOperation.Empty:
|
||||
// // Get all mails in the folder.
|
||||
break;
|
||||
case FolderOperation.Empty:
|
||||
var mailsToDelete = await _mailService.GetMailsByFolderIdAsync(folder.Id).ConfigureAwait(false);
|
||||
|
||||
// var mailsToDelete = await _folderService.GetMailByFolderIdAsync(folderStructure.FolderId).ConfigureAwait(false);
|
||||
change = new EmptyFolderRequest(folder, mailsToDelete);
|
||||
|
||||
// if (mailsToDelete.Any())
|
||||
// change = new FolderEmptyRequest(accountId, mailsToDelete.Select(a => a.Id).Distinct(), folderStructure.RemoteFolderId, folderStructure.FolderId);
|
||||
break;
|
||||
case FolderOperation.MarkAllAsRead:
|
||||
|
||||
// break;
|
||||
//case FolderOperation.Rename:
|
||||
// var newFolderName = await _dialogService.ShowRenameFolderDialogAsync(folderStructure.FolderName);
|
||||
var unreadItems = await _mailService.GetUnreadMailsByFolderIdAsync(folder.Id).ConfigureAwait(false);
|
||||
|
||||
// if (!string.IsNullOrEmpty(newFolderName))
|
||||
// change = new RenameFolderRequest(accountId, folderStructure.RemoteFolderId, folderStructure.FolderId, newFolderName, folderStructure.FolderName);
|
||||
if (unreadItems.Any())
|
||||
change = new MarkFolderAsReadRequest(folder, unreadItems);
|
||||
|
||||
// break;
|
||||
break;
|
||||
//case FolderOperation.Delete:
|
||||
// var isConfirmed = await _dialogService.ShowConfirmationDialogAsync($"'{folderStructure.FolderName}' is going to be deleted. Do you want to continue?", "Are you sure?", "Yes delete.");
|
||||
|
||||
|
||||
@@ -149,11 +149,10 @@ namespace Wino.Core.Synchronizers
|
||||
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
||||
|
||||
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
|
||||
// TODO: Outlook sends back the deleted Draft. Might be a bug in the graph API or in Wino.
|
||||
|
||||
var hasSendDraftRequest = batches.Any(a => a is BatchSendDraftRequestRequest);
|
||||
bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
|
||||
|
||||
if (hasSendDraftRequest && DelaySendOperationSynchronization())
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
@@ -227,8 +226,13 @@ namespace Wino.Core.Synchronizers
|
||||
changeRequestQueue.TryTake(out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +276,15 @@ namespace Wino.Core.Synchronizers
|
||||
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;
|
||||
@@ -287,12 +300,9 @@ namespace Wino.Core.Synchronizers
|
||||
/// </summary>
|
||||
/// <param name="batches">Batch requests to run in synchronization.</param>
|
||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||
private SynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(IEnumerable<IRequestBase> batches)
|
||||
private SynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(IEnumerable<IRequestBase> requests)
|
||||
{
|
||||
// TODO: Check folders only.
|
||||
var batchItems = batches.Where(a => a is IBatchChangeRequest).Cast<IBatchChangeRequest>();
|
||||
|
||||
var requests = batchItems.SelectMany(a => a.Items);
|
||||
bool isAllCustomSynchronizationRequests = requests.All(a => a is ICustomFolderSynchronizationRequest);
|
||||
|
||||
var options = new SynchronizationOptions()
|
||||
{
|
||||
@@ -300,9 +310,7 @@ namespace Wino.Core.Synchronizers
|
||||
Type = SynchronizationType.FoldersOnly
|
||||
};
|
||||
|
||||
bool isCustomSynchronization = requests.All(a => a is ICustomFolderSynchronizationRequest);
|
||||
|
||||
if (isCustomSynchronization)
|
||||
if (isAllCustomSynchronizationRequests)
|
||||
{
|
||||
// Gather FolderIds to synchronize.
|
||||
|
||||
@@ -327,6 +335,9 @@ namespace Wino.Core.Synchronizers
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> MoveToFocused(BatchMoveToFocusedRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> CreateDraft(BatchCreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> SendDraft(BatchSendDraftRequestRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Gmail.v1;
|
||||
@@ -245,111 +244,97 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var folderRequest = _gmailService.Users.Labels.List("me");
|
||||
|
||||
var labelsResponse = await folderRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (labelsResponse.Labels == null)
|
||||
try
|
||||
{
|
||||
_logger.Warning("No folders found for {Name}", Account.Name);
|
||||
return;
|
||||
}
|
||||
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var folderRequest = _gmailService.Users.Labels.List("me");
|
||||
|
||||
_logger.Debug($"Gmail folders found: {string.Join(",", labelsResponse.Labels.Select(a => a.Name))}");
|
||||
var labelsResponse = await folderRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Gmail has special Categories parent folder.
|
||||
var categoriesFolder = new MailItemFolder()
|
||||
{
|
||||
SpecialFolderType = SpecialFolderType.Category,
|
||||
MailAccountId = Account.Id,
|
||||
FolderName = "Categories",
|
||||
IsSynchronizationEnabled = false,
|
||||
IsSticky = true,
|
||||
Id = Guid.NewGuid(),
|
||||
RemoteFolderId = "Categories"
|
||||
};
|
||||
|
||||
var initializedFolders = new List<MailItemFolder>() { categoriesFolder };
|
||||
|
||||
foreach (var label in labelsResponse.Labels)
|
||||
{
|
||||
var localFolder = label.GetLocalFolder(Account.Id);
|
||||
|
||||
localFolder.MailAccountId = Account.Id;
|
||||
|
||||
initializedFolders.Add(localFolder);
|
||||
}
|
||||
|
||||
// 1. Create parent-child relations but first order by name desc to be able to not mess up FolderNames in memory.
|
||||
// 2. Mark special folder types that belong to categories folder.
|
||||
|
||||
var ordered = initializedFolders.OrderByDescending(a => a.FolderName);
|
||||
|
||||
// TODO: This can be refactored better.
|
||||
foreach (var label in ordered)
|
||||
{
|
||||
// Gmail categorizes sub-labels by '/' For example:
|
||||
// ParentTest/SubTest in the name of label means
|
||||
// SubTest is a sub-label of ParentTest label.
|
||||
|
||||
if (label.SpecialFolderType == SpecialFolderType.Promotions ||
|
||||
label.SpecialFolderType == SpecialFolderType.Updates ||
|
||||
label.SpecialFolderType == SpecialFolderType.Social ||
|
||||
label.SpecialFolderType == SpecialFolderType.Forums ||
|
||||
label.SpecialFolderType == SpecialFolderType.Personal)
|
||||
if (labelsResponse.Labels == null)
|
||||
{
|
||||
label.ParentRemoteFolderId = categoriesFolder.RemoteFolderId;
|
||||
|
||||
// These folders can not be a sub folder.
|
||||
continue;
|
||||
_logger.Warning("No folders found for {Name}", Account.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var isSubFolder = label.FolderName.Contains("/");
|
||||
List<MailItemFolder> insertedFolders = new();
|
||||
List<MailItemFolder> updatedFolders = new();
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
if (isSubFolder)
|
||||
// 1. Handle deleted labels.
|
||||
|
||||
foreach (var localFolder in localFolders)
|
||||
{
|
||||
var splittedFolderName = label.FolderName.Split('/');
|
||||
var partCount = splittedFolderName.Length;
|
||||
// Category folder is virtual folder for Wino. Skip it.
|
||||
if (localFolder.SpecialFolderType == SpecialFolderType.Category) continue;
|
||||
|
||||
if (partCount > 1)
|
||||
var remoteFolder = labelsResponse.Labels.FirstOrDefault(a => a.Id == localFolder.RemoteFolderId);
|
||||
|
||||
if (remoteFolder == null)
|
||||
{
|
||||
// Only make the last part connection since other relations will build up in the loop.
|
||||
// Local folder doesn't exists remotely. Delete local copy.
|
||||
await _gmailChangeProcessor.DeleteFolderAsync(Account.Id, localFolder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
var realChildFolderName = splittedFolderName[splittedFolderName.Length - 1];
|
||||
deletedFolders.Add(localFolder);
|
||||
}
|
||||
}
|
||||
|
||||
var childFolder = initializedFolders.Find(a => a.FolderName.EndsWith(realChildFolderName));
|
||||
// Delete the deleted folders from local list.
|
||||
deletedFolders.ForEach(a => localFolders.Remove(a));
|
||||
|
||||
string GetParentFolderName(string[] parts)
|
||||
// 2. Handle update/insert based on remote folders.
|
||||
foreach (var remoteFolder in labelsResponse.Labels)
|
||||
{
|
||||
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.Id);
|
||||
|
||||
if (existingLocalFolder == null)
|
||||
{
|
||||
// Insert new folder.
|
||||
var localFolder = remoteFolder.GetLocalFolder(labelsResponse, Account.Id);
|
||||
|
||||
insertedFolders.Add(localFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing folder. Right now we only update the name.
|
||||
|
||||
// TODO: Moving folders around different parents. This is not supported right now.
|
||||
// We will need more comphrensive folder update mechanism to support this.
|
||||
|
||||
if (ShouldUpdateFolder(remoteFolder, existingLocalFolder))
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
builder.Append(parts[i]);
|
||||
|
||||
if (i != parts.Length - 2)
|
||||
builder.Append("/");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
existingLocalFolder.FolderName = remoteFolder.Name;
|
||||
updatedFolders.Add(existingLocalFolder);
|
||||
}
|
||||
|
||||
var parentFolderName = GetParentFolderName(splittedFolderName);
|
||||
|
||||
var parentFolder = initializedFolders.Find(a => a.FolderName == parentFolderName);
|
||||
|
||||
if (childFolder != null && parentFolder != null)
|
||||
else
|
||||
{
|
||||
childFolder.FolderName = realChildFolderName;
|
||||
childFolder.ParentRemoteFolderId = parentFolder.RemoteFolderId;
|
||||
// Remove it from the local folder list to skip additional folder updates.
|
||||
localFolders.Remove(existingLocalFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _gmailChangeProcessor.UpdateFolderStructureAsync(Account.Id, initializedFolders).ConfigureAwait(false);
|
||||
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
|
||||
foreach (var folder in insertedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in updatedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUpdateFolder(Label remoteFolder, MailItemFolder existingLocalFolder)
|
||||
=> existingLocalFolder.FolderName.Equals(GoogleIntegratorExtensions.GetFolderName(remoteFolder), StringComparison.OrdinalIgnoreCase) == false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single get request to retrieve the raw message with the given id
|
||||
/// </summary>
|
||||
@@ -706,6 +691,34 @@ namespace Wino.Core.Synchronizers
|
||||
await _gmailChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<IClientServiceRequest>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateHttpBundleWithResponse<Label>(request, (item) =>
|
||||
{
|
||||
if (item is not RenameFolderRequest renameFolderRequest)
|
||||
throw new ArgumentException($"Renaming folder must be handled with '{nameof(RenameFolderRequest)}'");
|
||||
|
||||
var label = new Label()
|
||||
{
|
||||
Name = renameFolderRequest.NewFolderName
|
||||
};
|
||||
|
||||
return _gmailService.Users.Labels.Update(label, "me", request.Folder.RemoteFolderId);
|
||||
});
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<IClientServiceRequest>> EmptyFolder(EmptyFolderRequest request)
|
||||
{
|
||||
// Create batch delete request.
|
||||
|
||||
var deleteRequests = request.MailsToDelete.Select(a => new DeleteRequest(a));
|
||||
|
||||
return Delete(new BatchDeleteRequest(deleteRequests));
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<IClientServiceRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request)
|
||||
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)), true));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Execution
|
||||
|
||||
@@ -273,7 +273,13 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> Archive(BatchArchiveRequest request)
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> EmptyFolder(EmptyFolderRequest request)
|
||||
=> Delete(new BatchDeleteRequest(request.MailsToDelete.Select(a => new DeleteRequest(a))));
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request)
|
||||
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)), true));
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> SendDraft(BatchSendDraftRequestRequest request)
|
||||
{
|
||||
@@ -351,6 +357,15 @@ namespace Wino.Core.Synchronizers
|
||||
_clientPool.Release(client);
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateTaskBundle(async (ImapClient client) =>
|
||||
{
|
||||
var folder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false);
|
||||
await folder.RenameAsync(folder.ParentFolder, request.NewFolderName).ConfigureAwait(false);
|
||||
}, request);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
|
||||
@@ -406,10 +421,7 @@ namespace Wino.Core.Synchronizers
|
||||
// Therefore this should be avoided as many times as possible.
|
||||
|
||||
// This may create some inconsistencies, but nothing we can do...
|
||||
if (options.Type == SynchronizationType.FoldersOnly || options.Type == SynchronizationType.Full)
|
||||
{
|
||||
await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (options.Type != SynchronizationType.FoldersOnly)
|
||||
{
|
||||
@@ -492,7 +504,7 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
item.Request.RevertUIChanges();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
@@ -546,26 +558,20 @@ namespace Wino.Core.Synchronizers
|
||||
localFolder.SpecialFolderType = SpecialFolderType.Starred;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the local folder should be updated with the remote folder.
|
||||
/// </summary>
|
||||
/// <param name="remoteFolder">Remote folder</param>
|
||||
/// <param name="localFolder">Local folder.</param>
|
||||
private bool ShouldUpdateFolder(IMailFolder remoteFolder, MailItemFolder localFolder)
|
||||
{
|
||||
return remoteFolder.Name != localFolder.FolderName;
|
||||
}
|
||||
|
||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// https://www.rfc-editor.org/rfc/rfc4549#section-1.1
|
||||
|
||||
var localFolders = await _imapChangeProcessor.GetLocalIMAPFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var localFolders = await _imapChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
|
||||
ImapClient executorClient = null;
|
||||
|
||||
try
|
||||
{
|
||||
List<MailItemFolder> insertedFolders = new();
|
||||
List<MailItemFolder> updatedFolders = new();
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
executorClient = await _clientPool.GetClientAsync().ConfigureAwait(false);
|
||||
|
||||
var remoteFolders = (await executorClient.GetFoldersAsync(executorClient.PersonalNamespaces[0], cancellationToken: cancellationToken)).ToList();
|
||||
@@ -575,8 +581,6 @@ namespace Wino.Core.Synchronizers
|
||||
// 1.a If local folder doesn't exists remotely, delete it.
|
||||
// 1.b If local folder exists remotely, check if it is still a valid folder. If UidValidity is changed, delete it.
|
||||
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
foreach (var localFolder in localFolders)
|
||||
{
|
||||
IMailFolder remoteFolder = null;
|
||||
@@ -628,8 +632,6 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
var nameSpace = executorClient.PersonalNamespaces[0];
|
||||
|
||||
// var remoteFolders = (await executorClient.GetFoldersAsync(nameSpace, cancellationToken: cancellationToken)).ToList();
|
||||
|
||||
IMailFolder inbox = executorClient.Inbox;
|
||||
|
||||
// Sometimes Inbox is the root namespace. We need to check for that.
|
||||
@@ -684,18 +686,19 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
await remoteFolder.CloseAsync(cancellationToken: cancellationToken);
|
||||
|
||||
localFolders.Add(localFolder);
|
||||
insertedFolders.Add(localFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing folder. Right now we only update the name.
|
||||
|
||||
// TODO: Moving servers around different parents. This is not supported right now.
|
||||
// TODO: Moving folders around different parents. This is not supported right now.
|
||||
// We will need more comphrensive folder update mechanism to support this.
|
||||
|
||||
if (ShouldUpdateFolder(remoteFolder, existingLocalFolder))
|
||||
{
|
||||
existingLocalFolder.FolderName = remoteFolder.Name;
|
||||
updatedFolders.Add(existingLocalFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -705,13 +708,16 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
}
|
||||
|
||||
if (localFolders.Any())
|
||||
// Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
|
||||
foreach (var folder in insertedFolders)
|
||||
{
|
||||
await _imapChangeProcessor.UpdateFolderStructureAsync(Account.Id, localFolders);
|
||||
await _imapChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
|
||||
foreach (var folder in updatedFolders)
|
||||
{
|
||||
_logger.Information("No update is needed for imap folders.");
|
||||
await _imapChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -729,6 +735,8 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<IEnumerable<string>> SynchronizeFolderInternalAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!folder.IsSynchronizationEnabled) return default;
|
||||
@@ -978,5 +986,14 @@ namespace Wino.Core.Synchronizers
|
||||
_clientPool.Release(_synchronizationClient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether the local folder should be updated with the remote folder.
|
||||
/// IMAP only compares folder name for now.
|
||||
/// </summary>
|
||||
/// <param name="remoteFolder">Remote folder</param>
|
||||
/// <param name="localFolder">Local folder.</param>
|
||||
public bool ShouldUpdateFolder(IMailFolder remoteFolder, MailItemFolder localFolder) => remoteFolder.Name != localFolder.FolderName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace Wino.Core.Synchronizers
|
||||
"InternetMessageId",
|
||||
];
|
||||
|
||||
private readonly SemaphoreSlim _handleItemRetrievalSemaphore = new(1);
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<OutlookSynchronizer>();
|
||||
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
||||
private readonly GraphServiceClient _graphClient;
|
||||
@@ -184,7 +186,21 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
var messageIteratorAsync = PageIterator<Message, Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, messageCollectionPage, async (item) =>
|
||||
{
|
||||
return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken);
|
||||
try
|
||||
{
|
||||
await _handleItemRetrievalSemaphore.WaitAsync();
|
||||
return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error occurred while handling item {Id} for folder {FolderName}", item.Id, folder.FolderName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_handleItemRetrievalSemaphore.Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await messageIteratorAsync
|
||||
@@ -368,7 +384,7 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
graphFolders = await _graphClient.RequestAdapter.SendAsync(deltaRequest,
|
||||
Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse.CreateFromDiscriminatorValue,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -380,7 +396,7 @@ namespace Wino.Core.Synchronizers
|
||||
deltaRequest.QueryParameters.Add("%24deltaToken", currentDeltaLink);
|
||||
graphFolders = await _graphClient.RequestAdapter.SendAsync(deltaRequest,
|
||||
Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse.CreateFromDiscriminatorValue,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var iterator = PageIterator<MailFolder, Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, graphFolders, (folder) =>
|
||||
@@ -575,6 +591,8 @@ namespace Wino.Core.Synchronizers
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
|
||||
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
|
||||
MailKit.ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -583,6 +601,28 @@ namespace Wino.Core.Synchronizers
|
||||
await _outlookChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateHttpBundleWithResponse<MailFolder>(request, (item) =>
|
||||
{
|
||||
if (item is not RenameFolderRequest renameFolderRequest)
|
||||
throw new ArgumentException($"Renaming folder must be handled with '{nameof(RenameFolderRequest)}'");
|
||||
|
||||
var requestBody = new MailFolder
|
||||
{
|
||||
DisplayName = request.NewFolderName,
|
||||
};
|
||||
|
||||
return _graphClient.Me.MailFolders[request.Folder.RemoteFolderId].ToPatchRequestInformation(requestBody);
|
||||
});
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> EmptyFolder(EmptyFolderRequest request)
|
||||
=> Delete(new BatchDeleteRequest(request.MailsToDelete.Select(a => new DeleteRequest(a))));
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> MarkFolderAsRead(MarkFolderAsReadRequest request)
|
||||
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)), true));
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<RequestInformation>> batchedRequests, CancellationToken cancellationToken = default)
|
||||
@@ -646,7 +686,11 @@ namespace Wino.Core.Synchronizers
|
||||
HttpResponseMessage httpResponseMessage,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (bundle is HttpRequestBundle<RequestInformation, Message> messageBundle)
|
||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode));
|
||||
}
|
||||
else if (bundle is HttpRequestBundle<RequestInformation, Message> messageBundle)
|
||||
{
|
||||
var outlookMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
|
||||
|
||||
@@ -666,11 +710,6 @@ namespace Wino.Core.Synchronizers
|
||||
{
|
||||
// TODO: Handle mime retrieve message.
|
||||
}
|
||||
else if (!httpResponseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
// TODO: Should we even handle this?
|
||||
throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MimeMessage> DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<PackageReference Include="MailKit" Version="4.6.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.55.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.60.3" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.47.2" />
|
||||
<PackageReference Include="MimeKit" Version="4.6.0" />
|
||||
<PackageReference Include="morelinq" Version="4.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
Reference in New Issue
Block a user