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:
@@ -67,6 +67,9 @@ namespace Wino.Core.Domain.Entities
|
||||
return false;
|
||||
}
|
||||
|
||||
public static MailItemFolder CreateMoreFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.More, FolderName = Translator.MoreFolderNameOverride };
|
||||
public static MailItemFolder CreateCategoriesFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.Category, FolderName = Translator.CategoriesFolderNameOverride };
|
||||
|
||||
public override string ToString() => FolderName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
ChangeFlag,
|
||||
AlwaysMoveTo,
|
||||
MoveToFocused,
|
||||
Archive,
|
||||
RenameFolder,
|
||||
Archive
|
||||
EmptyFolder,
|
||||
MarkFolderRead,
|
||||
}
|
||||
|
||||
// UI requests
|
||||
|
||||
@@ -5,9 +5,17 @@ namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface IAccountMenuItem : IMenuItem
|
||||
{
|
||||
bool IsEnabled { get; set; }
|
||||
double SynchronizationProgress { get; set; }
|
||||
int UnreadItemCount { get; set; }
|
||||
IEnumerable<MailAccount> HoldingAccounts { get; }
|
||||
void UpdateAccount(MailAccount account);
|
||||
}
|
||||
|
||||
public interface IMergedAccountMenuItem : IAccountMenuItem
|
||||
{
|
||||
int MergedAccountCount { get; }
|
||||
|
||||
MergedInbox Parameter { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
Task<IMailItemFolder> ShowMoveMailFolderDialogAsync(List<IMailItemFolder> availableFolders);
|
||||
Task<AccountCreationDialogResult> ShowNewAccountMailProviderDialogAsync(List<IProviderDetail> availableProviders);
|
||||
IAccountCreationDialog GetAccountCreationDialog(MailProviderType type);
|
||||
Task<string> ShowTextInputDialogAsync(string currentInput, string dialogTitle, string dialogDescription);
|
||||
Task<string> ShowTextInputDialogAsync(string currentInput, string dialogTitle, string dialogDescription, string primaryButtonText);
|
||||
Task<MailAccount> ShowEditAccountDialogAsync(MailAccount account);
|
||||
Task<MailAccount> ShowAccountPickerDialogAsync(List<MailAccount> availableAccounts);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
int UnreadItemCount { get; set; }
|
||||
SpecialFolderType SpecialFolderType { get; }
|
||||
IEnumerable<IMailItemFolder> HandlingFolders { get; }
|
||||
IEnumerable<IMenuItem> SubMenuItems { get; }
|
||||
bool IsMoveTarget { get; }
|
||||
bool IsSticky { get; }
|
||||
bool IsSystemFolder { get; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
@@ -15,13 +16,10 @@ namespace Wino.Core.Domain.Interfaces
|
||||
Task<MailItemFolder> GetFolderAsync(Guid folderId);
|
||||
Task<MailItemFolder> GetFolderAsync(Guid accountId, string remoteFolderId);
|
||||
Task<List<MailItemFolder>> GetFoldersAsync(Guid accountId);
|
||||
Task<List<MailItemFolder>> GetUnreadUpdateFoldersAsync(Guid accountId);
|
||||
Task SetSpecialFolderAsync(Guid folderId, SpecialFolderType type);
|
||||
Task<MailItemFolder> GetSpecialFolderByAccountIdAsync(Guid accountId, SpecialFolderType type);
|
||||
Task<int> GetCurrentItemCountForFolder(Guid folderId);
|
||||
Task<int> GetFolderNotificationBadgeAsync(Guid folderId);
|
||||
Task ChangeStickyStatusAsync(Guid folderId, bool isSticky);
|
||||
Task UpdateCustomServerMailListAsync(Guid accountId, List<MailItemFolder> folders);
|
||||
|
||||
Task<MailAccount> UpdateSystemFolderConfigurationAsync(Guid accountId, SystemFolderConfiguration configuration);
|
||||
Task ChangeFolderSynchronizationStateAsync(Guid folderId, bool isSynchronizationEnabled);
|
||||
@@ -39,16 +37,6 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// </summary>
|
||||
Task<List<MailFolderPairMetadata>> GetMailFolderPairMetadatasAsync(string mailCopyId);
|
||||
|
||||
// v2
|
||||
|
||||
/// <summary>
|
||||
/// Performs bulk update for the given folders.
|
||||
/// Used in Gmail.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account that folders belong to.</param>
|
||||
/// <param name="allFolders">Folders to update.</param>
|
||||
Task BulkUpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the folder for the given account by remote folder id.
|
||||
/// </summary>
|
||||
@@ -84,12 +72,23 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// <param name="folderId">Folder to update.</param>
|
||||
Task UpdateFolderLastSyncDateAsync(Guid folderId);
|
||||
|
||||
Task TestAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the given folder.
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder to update.</param>
|
||||
Task UpdateFolderAsync(MailItemFolder folder);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the active folder menu items for the given account for UI.
|
||||
/// </summary>
|
||||
/// <param name="accountMenuItem">Account to get folder menu items for.</param>
|
||||
Task<IEnumerable<IMenuItem>> GetAccountFoldersForDisplayAsync(IAccountMenuItem accountMenuItem);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of unread item counts for the given account ids.
|
||||
/// Every folder that is marked as show unread badge is included.
|
||||
/// </summary>
|
||||
/// <param name="accountIds">Account ids to get unread folder counts for.</param>
|
||||
Task<List<UnreadItemCountResult>> GetUnreadItemCountResultsAsync(IEnumerable<Guid> accountIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@ namespace Wino.Core.Domain.Interfaces
|
||||
Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, MimeMessage generatedReplyMime, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null);
|
||||
Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options);
|
||||
|
||||
Task<List<string>> GetMailIdsByFolderIdAsync(Guid folderId);
|
||||
|
||||
// v2
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all mail copies for all folders.
|
||||
/// </summary>
|
||||
@@ -84,5 +80,17 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="mailCopyId">Native mail id of the message.</param>
|
||||
Task<bool> IsMailExistsAsync(string mailCopyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all mails for given folder id.
|
||||
/// </summary>
|
||||
/// <param name="folderId">Folder id to get mails for</param>
|
||||
Task<List<MailCopy>> GetMailsByFolderIdAsync(Guid folderId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all unread mails for given folder id.
|
||||
/// </summary>
|
||||
/// <param name="folderId">Folder id to get unread mails for.</param>
|
||||
Task<List<MailCopy>> GetUnreadMailsByFolderIdAsync(Guid folderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,15 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// Reverts the UI changes applied by <see cref="ApplyUIChanges"/> if the request fails.
|
||||
/// </summary>
|
||||
void RevertUIChanges();
|
||||
|
||||
/// <summary>
|
||||
/// Whether synchronizations should be delayed after executing this request.
|
||||
/// Specially Outlook sometimes don't report changes back immidiately after sending the API request.
|
||||
/// This results following synchronization to miss the changes.
|
||||
/// We add small delay for the following synchronization after executing current requests to overcome this issue.
|
||||
/// Default is false.
|
||||
/// </summary>
|
||||
bool DelayExecution { get; }
|
||||
}
|
||||
|
||||
public interface IRequest : IRequestBase
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
@@ -28,8 +27,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// <summary>
|
||||
/// Prepares requires IRequest collection for folder actions and executes them via proper synchronizers.
|
||||
/// </summary>
|
||||
/// <param name="operation">Folder operation to execute.</param>
|
||||
/// <param name="folderStructure">Target folder</param>
|
||||
Task ExecuteAsync(FolderOperation operation, IMailItemFolder folderStructure);
|
||||
/// <param name="folderOperationPreperationRequest">Folder prep request.</param>
|
||||
Task ExecuteAsync(FolderOperationPreperationRequest folderOperationPreperationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface IWinoRequestProcessor
|
||||
{
|
||||
Task<IRequest> PrepareFolderRequestAsync(FolderOperation operation, IMailItemFolder mailItemFolder);
|
||||
/// <summary>
|
||||
/// Prepares proper folder action requests for synchronizers to execute.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns>Base request that synchronizer can execute.</returns>
|
||||
Task<IRequestBase> PrepareFolderRequestAsync(FolderOperationPreperationRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Prepares proper Wino requests for synchronizers to execute categorized by AccountId and FolderId.
|
||||
@@ -17,6 +20,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// <param name="operation">User action</param>
|
||||
/// <param name="mailCopyIds">Selected mails.</param>
|
||||
/// <exception cref="UnavailableSpecialFolderException">When required folder target is not available for account.</exception>
|
||||
/// <returns>Base request that synchronizer can execute.</returns>
|
||||
Task<List<IRequest>> PrepareRequestsAsync(MailOperationPreperationRequest request);
|
||||
}
|
||||
}
|
||||
|
||||
13
Wino.Core.Domain/Models/Accounts/UnreadItemCountResult.cs
Normal file
13
Wino.Core.Domain/Models/Accounts/UnreadItemCountResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Accounts
|
||||
{
|
||||
public class UnreadItemCountResult
|
||||
{
|
||||
public Guid FolderId { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public SpecialFolderType SpecialFolderType { get; set; }
|
||||
public int UnreadItemCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Folders
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates a request to prepare a folder operation like Rename, Delete, etc.
|
||||
/// </summary>
|
||||
/// <param name="Action">Folder operation.</param>
|
||||
/// <param name="Folder">Target folder.</param>
|
||||
public record FolderOperationPreperationRequest(FolderOperation Action, MailItemFolder Folder) { }
|
||||
}
|
||||
@@ -9,61 +9,31 @@ namespace Wino.Core.Domain.Models.MailItem
|
||||
/// <summary>
|
||||
/// Encapsulates the options for preparing requests to execute mail operations for mail items like Move, Delete, MarkAsRead, etc.
|
||||
/// </summary>
|
||||
public class MailOperationPreperationRequest
|
||||
/// <param name="Action"> Action to execute. </param>
|
||||
/// <param name="MailItems"> Mail copies execute the action on. </param>
|
||||
/// <param name="ToggleExecution"> Whether the operation can be reverted if needed.
|
||||
/// eg. MarkAsRead on already read item will set the action to MarkAsUnread.
|
||||
/// This is used in hover actions for example. </param>
|
||||
/// <param name="IgnoreHardDeleteProtection"> Whether hard delete protection should be ignored.
|
||||
/// Discard draft requests for example should ignore hard delete protection. </param>
|
||||
/// <param name="MoveTargetFolder"> Moving folder for the Move operation.
|
||||
/// If null and the action is Move, the user will be prompted to select a folder. </param>
|
||||
public record MailOperationPreperationRequest(MailOperation Action, IEnumerable<MailCopy> MailItems, bool ToggleExecution, bool IgnoreHardDeleteProtection, IMailItemFolder MoveTargetFolder)
|
||||
{
|
||||
public MailOperationPreperationRequest(MailOperation action,
|
||||
IEnumerable<MailCopy> mailItems,
|
||||
bool toggleExecution = false,
|
||||
IMailItemFolder moveTargetFolder = null,
|
||||
bool ignoreHardDeleteProtection = false)
|
||||
bool ignoreHardDeleteProtection = false) : this(action, mailItems ?? throw new ArgumentNullException(nameof(mailItems)), toggleExecution, ignoreHardDeleteProtection, moveTargetFolder)
|
||||
{
|
||||
Action = action;
|
||||
MailItems = mailItems ?? throw new ArgumentNullException(nameof(mailItems));
|
||||
ToggleExecution = toggleExecution;
|
||||
MoveTargetFolder = moveTargetFolder;
|
||||
IgnoreHardDeleteProtection = ignoreHardDeleteProtection;
|
||||
}
|
||||
|
||||
public MailOperationPreperationRequest(MailOperation action,
|
||||
MailCopy singleMailItem,
|
||||
bool toggleExecution = false,
|
||||
IMailItemFolder moveTargetFolder = null,
|
||||
bool ignoreHardDeleteProtection = false)
|
||||
bool ignoreHardDeleteProtection = false) : this(action, new List<MailCopy>() { singleMailItem }, toggleExecution, ignoreHardDeleteProtection, moveTargetFolder)
|
||||
{
|
||||
Action = action;
|
||||
MailItems = new List<MailCopy>() { singleMailItem };
|
||||
ToggleExecution = toggleExecution;
|
||||
MoveTargetFolder = moveTargetFolder;
|
||||
IgnoreHardDeleteProtection = ignoreHardDeleteProtection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action to execute.
|
||||
/// </summary>
|
||||
public MailOperation Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mail copies execute the action on.
|
||||
/// </summary>
|
||||
public IEnumerable<MailCopy> MailItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the operation can be reverted if needed.
|
||||
/// eg. MarkAsRead on already read item will set the action to MarkAsUnread.
|
||||
/// This is used in hover actions for example.
|
||||
/// </summary>
|
||||
public bool ToggleExecution { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether hard delete protection should be ignored.
|
||||
/// Discard draft requests for example should ignore hard delete protection.
|
||||
/// </summary>
|
||||
public bool IgnoreHardDeleteProtection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Moving folder for the Move operation.
|
||||
/// If null and the action is Move, the user will be prompted to select a folder.
|
||||
/// </summary>
|
||||
public IMailItemFolder MoveTargetFolder { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
public enum NavigationTransitionType
|
||||
{
|
||||
None, // Supress
|
||||
DrillIn,
|
||||
DrillIn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,23 @@ namespace Wino.Core.Domain.Models.Requests
|
||||
public abstract IBatchChangeRequest CreateBatch(IEnumerable<IRequest> requests);
|
||||
public abstract void ApplyUIChanges();
|
||||
public abstract void RevertUIChanges();
|
||||
|
||||
public virtual bool DelayExecution => false;
|
||||
}
|
||||
|
||||
public abstract record FolderRequestBase(MailItemFolder Folder, MailSynchronizerOperation Operation) : IFolderRequest
|
||||
{
|
||||
public abstract void ApplyUIChanges();
|
||||
public abstract void RevertUIChanges();
|
||||
|
||||
public virtual bool DelayExecution => false;
|
||||
}
|
||||
|
||||
public abstract record BatchRequestBase(IEnumerable<IRequest> Items, MailSynchronizerOperation Operation) : IBatchChangeRequest
|
||||
{
|
||||
public abstract void ApplyUIChanges();
|
||||
public abstract void RevertUIChanges();
|
||||
|
||||
public virtual bool DelayExecution => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
"DialogMessage_UnlinkAccountsConfirmationTitle": "Unlink Accounts",
|
||||
"DialogMessage_EmptySubjectConfirmation": "Missin Subject",
|
||||
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
|
||||
"DialogMessage_RenameFolderTitle": "Rename Folder",
|
||||
"DialogMessage_RenameFolderMessage": "Enter new name for this folder",
|
||||
"DialogMessage_UnsubscribeConfirmationTitle": "Unsubscribe",
|
||||
"DialogMessage_UnsubscribeConfirmationOneClickMessage": "Do you want to stop getting messages from {0}?",
|
||||
"DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "To stop getting messages from {0}, go to their website to unsubscribe.",
|
||||
|
||||
535
Wino.Core.Domain/Translator.Designer.cs
generated
535
Wino.Core.Domain/Translator.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -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" />
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Wino.Mail.ViewModels
|
||||
[RelayCommand]
|
||||
private async Task CreateMergedAccountAsync()
|
||||
{
|
||||
var linkName = await DialogService.ShowTextInputDialogAsync(string.Empty, Translator.DialogMessage_CreateLinkedAccountTitle, Translator.DialogMessage_CreateLinkedAccountMessage);
|
||||
var linkName = await DialogService.ShowTextInputDialogAsync(string.Empty, Translator.DialogMessage_CreateLinkedAccountTitle, Translator.DialogMessage_CreateLinkedAccountMessage, Translator.Buttons_Create);
|
||||
|
||||
if (string.IsNullOrEmpty(linkName)) return;
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.MenuItems;
|
||||
using Wino.Core.Messages.Accounts;
|
||||
using Wino.Core.Messages.Mails;
|
||||
@@ -49,8 +48,8 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
private IAccountMenuItem latestSelectedAccountMenuItem;
|
||||
|
||||
public MenuItemCollection FooterItems { get; set; } = [];
|
||||
public MenuItemCollection MenuItems { get; set; } = [];
|
||||
public MenuItemCollection FooterItems { get; set; }
|
||||
public MenuItemCollection MenuItems { get; set; }
|
||||
|
||||
private readonly SettingsItem SettingsItem = new SettingsItem();
|
||||
|
||||
@@ -118,6 +117,14 @@ namespace Wino.Mail.ViewModels
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
|
||||
MenuItems = new MenuItemCollection(Dispatcher);
|
||||
FooterItems = new MenuItemCollection(Dispatcher);
|
||||
}
|
||||
|
||||
public IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folder)
|
||||
{
|
||||
if (folder == null || folder.SpecialFolderType == SpecialFolderType.Category || folder.SpecialFolderType == SpecialFolderType.More)
|
||||
@@ -148,183 +155,63 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
private async Task LoadAccountsAsync()
|
||||
{
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
// First clear all account menu items.
|
||||
MenuItems.RemoveRange(MenuItems.Where(a => a is IAccountMenuItem));
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
// Group accounts by merged account.
|
||||
var groupedAccounts = accounts.GroupBy(a => a.MergedInboxId);
|
||||
var groupedAccounts = accounts.GroupBy(a => a.MergedInboxId).OrderBy(a => a.Key != null);
|
||||
|
||||
foreach (var accountGroup in groupedAccounts)
|
||||
foreach (var group in groupedAccounts)
|
||||
{
|
||||
var mergedInbox = accountGroup.Key;
|
||||
var mergedInboxId = group.Key;
|
||||
|
||||
if (mergedInbox == null)
|
||||
if (mergedInboxId == null)
|
||||
{
|
||||
// This account is not merged. Create menu item for each account.
|
||||
foreach (var account in accountGroup)
|
||||
// Single accounts.
|
||||
// Preserve the order while listing.
|
||||
|
||||
var orderedGroup = group.OrderBy(a => a.Order);
|
||||
|
||||
foreach (var account in orderedGroup)
|
||||
{
|
||||
await CreateNestedAccountMenuItem(account);
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MenuItems.Add(new AccountMenuItem(account, null));
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Accounts are merged. Create menu item for merged inbox.
|
||||
await CreateMergedInboxMenuItemAsync(accountGroup);
|
||||
// Merged accounts.
|
||||
var mergedInbox = group.First().MergedInbox;
|
||||
var mergedAccountMenuItem = new MergedAccountMenuItem(mergedInbox, group, null);
|
||||
|
||||
foreach (var accountItem in group)
|
||||
{
|
||||
mergedAccountMenuItem.SubMenuItems.Add(new AccountMenuItem(accountItem, mergedAccountMenuItem));
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MenuItems.Add(mergedAccountMenuItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Re-assign latest selected account menu item for containers to reflect changes better.
|
||||
// Also , this will ensure that the latest selected account is still selected after re-creation.
|
||||
|
||||
if (latestSelectedAccountMenuItem != null)
|
||||
{
|
||||
latestSelectedAccountMenuItem = MenuItems.GetAccountMenuItem(latestSelectedAccountMenuItem.EntityId.GetValueOrDefault());
|
||||
|
||||
if (latestSelectedAccountMenuItem != null)
|
||||
{
|
||||
latestSelectedAccountMenuItem.IsSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnFolderUpdated(MailItemFolder updatedFolder, MailAccount account)
|
||||
{
|
||||
base.OnFolderUpdated(updatedFolder, account);
|
||||
|
||||
if (updatedFolder == null) return;
|
||||
|
||||
var folderMenuItemsToUpdate = MenuItems.GetFolderItems(updatedFolder.Id);
|
||||
|
||||
foreach (var item in folderMenuItemsToUpdate)
|
||||
if (latestSelectedAccountMenuItem != null && MenuItems.TryGetAccountMenuItem(latestSelectedAccountMenuItem.EntityId.GetValueOrDefault(), out IAccountMenuItem foundLatestSelectedAccountMenuItem))
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
item.UpdateFolder(updatedFolder);
|
||||
foundLatestSelectedAccountMenuItem.IsSelected = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateMergedInboxMenuItemAsync(IEnumerable<MailAccount> accounts)
|
||||
{
|
||||
var mergedInbox = accounts.First().MergedInbox;
|
||||
var mergedInboxMenuItem = new MergedAccountMenuItem(mergedInbox, null); // Merged accounts are parentless.
|
||||
|
||||
// Store common special type folders.
|
||||
var commonFolderList = new Dictionary<MailAccount, IMailItemFolder>();
|
||||
|
||||
// Map special folder types for each account.
|
||||
var accountTreeList = new List<AccountFolderTree>();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var accountStructure = await _folderService.GetFolderStructureForAccountAsync(account.Id, includeHiddenFolders: true);
|
||||
accountTreeList.Add(accountStructure);
|
||||
}
|
||||
|
||||
var allFolders = accountTreeList.SelectMany(a => a.Folders);
|
||||
|
||||
// 1. Group sticky folders by special folder type.
|
||||
// 2. Merge all folders that are sticky and have the same special folder type.
|
||||
// 3. Add merged folder menu items to the merged inbox menu item.
|
||||
// 4. Add remaining sticky folders that doesn't exist in all accounts as plain folder menu items.
|
||||
|
||||
var stickyFolders = allFolders.Where(a => a.IsSticky);
|
||||
|
||||
var grouped = stickyFolders
|
||||
.GroupBy(a => a.SpecialFolderType)
|
||||
.Where(a => accountTreeList.All(b => b.HasSpecialTypeFolder(a.Key)));
|
||||
|
||||
var mergedInboxItems = grouped.Select(a => new MergedAccountFolderMenuItem(a.ToList(), mergedInboxMenuItem, mergedInbox));
|
||||
|
||||
// Shared common folders.
|
||||
foreach (var mergedInboxFolder in mergedInboxItems)
|
||||
{
|
||||
mergedInboxMenuItem.SubMenuItems.Add(mergedInboxFolder);
|
||||
}
|
||||
|
||||
var usedFolderIds = mergedInboxItems.SelectMany(a => a.Parameter.Select(a => a.Id));
|
||||
var remainingStickyFolders = stickyFolders.Where(a => !usedFolderIds.Contains(a.Id));
|
||||
|
||||
// Marked as sticky, but doesn't exist in all accounts. Add as plain folder menu item.
|
||||
foreach (var remainingStickyFolder in remainingStickyFolders)
|
||||
{
|
||||
var account = accounts.FirstOrDefault(a => a.Id == remainingStickyFolder.MailAccountId);
|
||||
mergedInboxMenuItem.SubMenuItems.Add(new FolderMenuItem(remainingStickyFolder, account, mergedInboxMenuItem));
|
||||
}
|
||||
|
||||
|
||||
var mergedMoreItem = new MergedAccountMoreFolderMenuItem(null, null, mergedInboxMenuItem);
|
||||
|
||||
// 2. Sticky folder preparation is done. Continue with regular account menu items.
|
||||
|
||||
foreach (var accountTree in accountTreeList)
|
||||
{
|
||||
var tree = accountTree.GetAccountMenuTree(mergedInboxMenuItem);
|
||||
|
||||
mergedMoreItem.SubMenuItems.Add(tree);
|
||||
}
|
||||
|
||||
mergedInboxMenuItem.SubMenuItems.Add(mergedMoreItem);
|
||||
|
||||
MenuItems.Add(mergedInboxMenuItem);
|
||||
|
||||
// Instead of refreshing all accounts, refresh the merged account only.
|
||||
// Receiver will handle it.
|
||||
|
||||
Messenger.Send(new RefreshUnreadCountsMessage(mergedInbox.Id));
|
||||
}
|
||||
|
||||
private async Task<IAccountMenuItem> CreateNestedAccountMenuItem(MailAccount account)
|
||||
{
|
||||
try
|
||||
{
|
||||
await accountInitFolderUpdateSlim.WaitAsync();
|
||||
|
||||
// Don't remove but replace existing record.
|
||||
int existingIndex = -1;
|
||||
|
||||
var existingAccountMenuItem = MenuItems.FirstOrDefault(a => a is AccountMenuItem accountMenuItem && accountMenuItem.Parameter.Id == account.Id);
|
||||
|
||||
if (existingAccountMenuItem != null)
|
||||
{
|
||||
existingIndex = MenuItems.IndexOf(existingAccountMenuItem);
|
||||
}
|
||||
|
||||
// Create account structure with integrator for this menu item.
|
||||
var accountStructure = await _folderService.GetFolderStructureForAccountAsync(account.Id, includeHiddenFolders: false);
|
||||
|
||||
var createdMenuItem = accountStructure.GetAccountMenuTree();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
createdMenuItem.IsExpanded = existingAccountMenuItem.IsExpanded;
|
||||
|
||||
MenuItems.RemoveAt(existingIndex);
|
||||
MenuItems.Insert(existingIndex, createdMenuItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
MenuItems.AddAccountMenuItem(createdMenuItem);
|
||||
}
|
||||
});
|
||||
|
||||
Messenger.Send(new RefreshUnreadCountsMessage(account.Id));
|
||||
|
||||
return createdMenuItem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, WinoErrors.AccountStructureRender);
|
||||
}
|
||||
finally
|
||||
{
|
||||
accountInitFolderUpdateSlim.Release();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
@@ -371,7 +258,6 @@ namespace Wino.Mail.ViewModels
|
||||
Type = SynchronizationType.Inbox
|
||||
};
|
||||
|
||||
|
||||
Messenger.Send(new NewSynchronizationRequested(options));
|
||||
}
|
||||
}
|
||||
@@ -392,9 +278,9 @@ namespace Wino.Mail.ViewModels
|
||||
// Find the account that this folder and mail belongs to.
|
||||
var account = await _mailService.GetMailAccountByUniqueIdAsync(accountExtendedMessage.NavigateMailItem.UniqueId).ConfigureAwait(false);
|
||||
|
||||
if (account != null && MenuItems.GetAccountMenuItem(account.Id) is IAccountMenuItem accountMenuItem)
|
||||
if (account != null && MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem))
|
||||
{
|
||||
ChangeLoadedAccount(accountMenuItem);
|
||||
await ChangeLoadedAccountAsync(accountMenuItem);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(accountExtendedMessage);
|
||||
|
||||
@@ -402,7 +288,7 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessLaunchDefault();
|
||||
await ProcessLaunchDefaultAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,7 +305,7 @@ namespace Wino.Mail.ViewModels
|
||||
else
|
||||
{
|
||||
// Use default startup extending.
|
||||
ProcessLaunchDefault();
|
||||
await ProcessLaunchDefaultAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +315,7 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessLaunchDefault()
|
||||
private async Task ProcessLaunchDefaultAsync()
|
||||
{
|
||||
if (PreferencesService.StartupEntityId == null)
|
||||
{
|
||||
@@ -452,7 +338,7 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
if (startupEntityMenuItem is IAccountMenuItem startupAccountMenuItem)
|
||||
{
|
||||
ChangeLoadedAccount(startupAccountMenuItem);
|
||||
await ChangeLoadedAccountAsync(startupAccountMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,30 +356,49 @@ namespace Wino.Mail.ViewModels
|
||||
var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, mailInitCompletionSource);
|
||||
|
||||
NavigationService.NavigateFolder(args);
|
||||
StatePersistenceService.CoreWindowTitle = $"{baseFolderMenuItem.AssignedAccountName} - {baseFolderMenuItem.FolderName}";
|
||||
|
||||
UpdateWindowTitleForFolder(baseFolderMenuItem);
|
||||
|
||||
// Wait until mail list page picks up the event and finish initialization of the mails.
|
||||
await mailInitCompletionSource.Task;
|
||||
}
|
||||
|
||||
private void UpdateWindowTitleForFolder(IBaseFolderMenuItem folder)
|
||||
{
|
||||
StatePersistenceService.CoreWindowTitle = $"{folder.AssignedAccountName} - {folder.FolderName}";
|
||||
}
|
||||
|
||||
private async Task NavigateSpecialFolderAsync(MailAccount account, SpecialFolderType specialFolderType, bool extendAccountMenu)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (account == null) return;
|
||||
|
||||
// If the account is inside a merged account, expand the merged account and navigate to shared folder.
|
||||
if (MenuItems.TryGetMergedAccountRootFolderMenuItemByAccountId(account.Id, specialFolderType, out MergedAccountFolderMenuItem mergedFolderItem))
|
||||
{
|
||||
mergedFolderItem.Expand();
|
||||
await NavigateFolderAsync(mergedFolderItem);
|
||||
}
|
||||
else if (MenuItems.TryGetRootSpecialFolderMenuItem(account.Id, specialFolderType, out FolderMenuItem rootFolderMenuItem))
|
||||
{
|
||||
// Account is not in merged account. Navigate to root folder.
|
||||
if (!MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem)) return;
|
||||
|
||||
rootFolderMenuItem.Expand();
|
||||
await NavigateFolderAsync(rootFolderMenuItem);
|
||||
// First make sure to navigate to the given accounnt.
|
||||
|
||||
if (latestSelectedAccountMenuItem != accountMenuItem)
|
||||
{
|
||||
await ChangeLoadedAccountAsync(accountMenuItem, false);
|
||||
}
|
||||
|
||||
// Account folders are already initialized.
|
||||
// Try to find the special folder menu item and navigate to it.
|
||||
|
||||
if (latestSelectedAccountMenuItem is IMergedAccountMenuItem latestMergedAccountMenuItem)
|
||||
{
|
||||
if (MenuItems.TryGetMergedAccountSpecialFolderMenuItem(latestSelectedAccountMenuItem.EntityId.Value, specialFolderType, out IBaseFolderMenuItem mergedFolderMenuItem))
|
||||
{
|
||||
await NavigateFolderAsync(mergedFolderMenuItem);
|
||||
}
|
||||
}
|
||||
else if (latestSelectedAccountMenuItem is IAccountMenuItem latestAccountMenuItem)
|
||||
{
|
||||
if (MenuItems.TryGetSpecialFolderMenuItem(account.Id, specialFolderType, out FolderMenuItem rootFolderMenuItem))
|
||||
{
|
||||
await NavigateFolderAsync(rootFolderMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -566,7 +471,12 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
foreach (var folder in folderMenuItem.HandlingFolders)
|
||||
{
|
||||
await _winoRequestDelegator.ExecuteAsync(operation, folder);
|
||||
if (folder is MailItemFolder realFolder)
|
||||
{
|
||||
var folderPrepRequest = new FolderOperationPreperationRequest(operation, realFolder);
|
||||
|
||||
await _winoRequestDelegator.ExecuteAsync(folderPrepRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the pins.
|
||||
@@ -632,6 +542,11 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
await NavigateFolderAsync(baseFolderMenuItem);
|
||||
}
|
||||
else if (clickedMenuItem is MergedAccountMenuItem clickedMergedAccountMenuItem && latestSelectedAccountMenuItem != clickedMenuItem)
|
||||
{
|
||||
// Don't navigate to merged account if it's already selected. Preserve user's already selected folder.
|
||||
await ChangeLoadedAccountAsync(clickedMergedAccountMenuItem, true);
|
||||
}
|
||||
else if (clickedMenuItem is SettingsItem)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.SettingsPage);
|
||||
@@ -642,19 +557,23 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem && latestSelectedAccountMenuItem != clickedAccountMenuItem)
|
||||
{
|
||||
ChangeLoadedAccount(clickedAccountMenuItem);
|
||||
await ChangeLoadedAccountAsync(clickedAccountMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ChangeLoadedAccount(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true)
|
||||
private async Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true)
|
||||
{
|
||||
if (clickedBaseAccountMenuItem == null) return;
|
||||
|
||||
// User clicked an account in Windows Mail style menu.
|
||||
// List folders for this account and select Inbox.
|
||||
|
||||
await MenuItems.SetAccountMenuItemEnabledStatusAsync(false);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
clickedBaseAccountMenuItem.IsEnabled = false;
|
||||
|
||||
if (latestSelectedAccountMenuItem != null)
|
||||
{
|
||||
latestSelectedAccountMenuItem.IsSelected = false;
|
||||
@@ -662,20 +581,18 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
clickedBaseAccountMenuItem.IsSelected = true;
|
||||
|
||||
|
||||
latestSelectedAccountMenuItem = clickedBaseAccountMenuItem;
|
||||
|
||||
|
||||
if (clickedBaseAccountMenuItem is AccountMenuItem accountMenuItem)
|
||||
{
|
||||
MenuItems.ReplaceFolders(accountMenuItem.SubMenuItems);
|
||||
}
|
||||
else if (clickedBaseAccountMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
|
||||
{
|
||||
MenuItems.ReplaceFolders(mergedAccountMenuItem.SubMenuItems);
|
||||
}
|
||||
});
|
||||
|
||||
// Load account folder structure and replace the visible folders.
|
||||
var folders = await _folderService.GetAccountFoldersForDisplayAsync(clickedBaseAccountMenuItem);
|
||||
|
||||
// var unreadCountResult = await _folderService.GetUnreadItemCountResultsAsync(clickedBaseAccountMenuItem.HoldingAccounts.Select(a => a.Id)).ConfigureAwait(false);
|
||||
|
||||
await MenuItems.ReplaceFoldersAsync(folders);
|
||||
await UpdateUnreadItemCountAsync();
|
||||
await MenuItems.SetAccountMenuItemEnabledStatusAsync(true);
|
||||
|
||||
if (navigateInbox)
|
||||
{
|
||||
await Task.Yield();
|
||||
@@ -687,6 +604,67 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUnreadItemCountAsync()
|
||||
{
|
||||
// Get visible account menu items, ordered by merged accounts at the last.
|
||||
// We will update the unread counts for all single accounts and trigger UI refresh for merged menu items.
|
||||
var accountMenuItems = MenuItems.GetAllAccountMenuItems().OrderBy(a => a.HoldingAccounts.Count());
|
||||
|
||||
// Individually get all single accounts' unread counts.
|
||||
var accountIds = accountMenuItems.OfType<AccountMenuItem>().Select(a => a.AccountId);
|
||||
var unreadCountResult = await _folderService.GetUnreadItemCountResultsAsync(accountIds).ConfigureAwait(false);
|
||||
|
||||
// Recursively update all folders' unread counts to 0.
|
||||
// Query above only returns unread counts that exists. We need to reset the rest to 0 first.
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MenuItems.UpdateUnreadItemCountsToZero();
|
||||
});
|
||||
|
||||
foreach (var accountMenuItem in accountMenuItems)
|
||||
{
|
||||
if (accountMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
mergedAccountMenuItem.RefreshFolderItemCount();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
accountMenuItem.UnreadItemCount = unreadCountResult
|
||||
.Where(a => a.AccountId == accountMenuItem.HoldingAccounts.First().Id && a.SpecialFolderType == SpecialFolderType.Inbox)
|
||||
.Sum(a => a.UnreadItemCount);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Try to update unread counts for all folders.
|
||||
foreach (var unreadCount in unreadCountResult)
|
||||
{
|
||||
if (MenuItems.TryGetFolderMenuItem(unreadCount.FolderId, out IBaseFolderMenuItem folderMenuItem))
|
||||
{
|
||||
if (folderMenuItem is IMergedAccountFolderMenuItem mergedAccountFolderMenuItem)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
folderMenuItem.UnreadItemCount = unreadCountResult.Where(a => a.SpecialFolderType == unreadCount.SpecialFolderType && mergedAccountFolderMenuItem.HandlingFolders.Select(b => b.Id).Contains(a.FolderId)).Sum(a => a.UnreadItemCount);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
folderMenuItem.UnreadItemCount = unreadCount.UnreadItemCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void NavigateInbox(IAccountMenuItem clickedBaseAccountMenuItem)
|
||||
{
|
||||
if (clickedBaseAccountMenuItem is AccountMenuItem accountMenuItem)
|
||||
@@ -741,7 +719,16 @@ namespace Wino.Mail.ViewModels
|
||||
else
|
||||
{
|
||||
// There are multiple accounts and there is no selection.
|
||||
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(accounts));
|
||||
// Don't list all accounts, but only accounts that belong to Merged Inbox.
|
||||
|
||||
if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem)
|
||||
{
|
||||
var mergedAccounts = accounts.Where(a => a.MergedInboxId == selectedMergedAccountMenuItem.EntityId);
|
||||
|
||||
if (!mergedAccounts.Any()) return;
|
||||
|
||||
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(mergedAccounts.ToList()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,18 +838,26 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
|
||||
protected override async void OnAccountUpdated(MailAccount updatedAccount)
|
||||
=> await ExecuteUIThread(() => { MenuItems.GetAccountMenuItem(updatedAccount.Id)?.UpdateAccount(updatedAccount); });
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem))
|
||||
{
|
||||
foundAccountMenuItem.UpdateAccount(updatedAccount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnAccountRemoved(MailAccount removedAccount)
|
||||
=> Messenger.Send(new AccountsMenuRefreshRequested(true));
|
||||
|
||||
protected override async void OnAccountCreated(MailAccount createdAccount)
|
||||
{
|
||||
var createdMenuItem = await CreateNestedAccountMenuItem(createdAccount);
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
if (createdMenuItem == null) return;
|
||||
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) return;
|
||||
|
||||
ChangeLoadedAccount(createdMenuItem);
|
||||
await ChangeLoadedAccountAsync(createdMenuItem);
|
||||
|
||||
// Each created account should start a new synchronization automatically.
|
||||
var options = new SynchronizationOptions()
|
||||
@@ -876,96 +871,9 @@ namespace Wino.Mail.ViewModels
|
||||
await _nativeAppService.PinAppToTaskbarAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates given single account menu item's unread count for all folders.
|
||||
/// </summary>
|
||||
/// <param name="accountMenuItem">Menu item to update unread count for.</param>
|
||||
/// <returns>Unread item count for Inbox only.</returns>
|
||||
private async Task<int> UpdateSingleAccountMenuItemUnreadCountAsync(AccountMenuItem accountMenuItem)
|
||||
{
|
||||
var accountId = accountMenuItem.AccountId;
|
||||
int inboxItemCount = 0;
|
||||
|
||||
// Get the folders needed to be refreshed.
|
||||
var allFolders = await _folderService.GetUnreadUpdateFoldersAsync(accountId);
|
||||
|
||||
foreach (var folder in allFolders)
|
||||
{
|
||||
var unreadItemCount = await UpdateAccountFolderUnreadItemCountAsync(accountMenuItem, folder.Id);
|
||||
|
||||
if (folder.SpecialFolderType == SpecialFolderType.Inbox)
|
||||
{
|
||||
inboxItemCount = unreadItemCount;
|
||||
|
||||
await ExecuteUIThread(() => { accountMenuItem.UnreadItemCount = unreadItemCount; });
|
||||
}
|
||||
}
|
||||
|
||||
return inboxItemCount;
|
||||
}
|
||||
|
||||
private async Task RefreshUnreadCountsForAccountAsync(Guid accountId)
|
||||
{
|
||||
// TODO: Merged accounts unread item count.
|
||||
|
||||
var accountMenuItem = MenuItems.GetAccountMenuItem(accountId);
|
||||
|
||||
if (accountMenuItem == null) return;
|
||||
|
||||
if (accountMenuItem is AccountMenuItem singleAccountMenuItem)
|
||||
{
|
||||
await UpdateSingleAccountMenuItemUnreadCountAsync(singleAccountMenuItem);
|
||||
|
||||
}
|
||||
else if (accountMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
|
||||
{
|
||||
// Merged account.
|
||||
// Root account should include all parent accounts' unread item count.
|
||||
|
||||
int totalUnreadCount = 0;
|
||||
|
||||
var individualAccountMenuItems = mergedAccountMenuItem.GetAccountMenuItems();
|
||||
|
||||
foreach (var singleMenuItem in individualAccountMenuItems)
|
||||
{
|
||||
totalUnreadCount += await UpdateSingleAccountMenuItemUnreadCountAsync(singleMenuItem);
|
||||
}
|
||||
|
||||
// At this point all single accounts are calculated.
|
||||
// Merge account folder's menu items can be calculated from those values for precision.
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
mergedAccountMenuItem.RefreshFolderItemCount();
|
||||
mergedAccountMenuItem.UnreadItemCount = totalUnreadCount;
|
||||
});
|
||||
}
|
||||
|
||||
await ExecuteUIThread(async () => { await _notificationBuilder.UpdateTaskbarIconBadgeAsync(); });
|
||||
}
|
||||
|
||||
private async Task<int> UpdateAccountFolderUnreadItemCountAsync(AccountMenuItem accountMenuItem, Guid folderId)
|
||||
{
|
||||
if (accountMenuItem == null) return 0;
|
||||
|
||||
var folder = accountMenuItem.FlattenedFolderHierarchy.Find(a => a.Parameter?.Id == folderId);
|
||||
|
||||
if (folder == null) return 0;
|
||||
|
||||
int folderUnreadItemCount = 0;
|
||||
|
||||
folderUnreadItemCount = await _folderService.GetFolderNotificationBadgeAsync(folder.Parameter.Id).ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() => { folder.UnreadItemCount = folderUnreadItemCount; });
|
||||
|
||||
return folderUnreadItemCount;
|
||||
}
|
||||
|
||||
private async Task SetAccountAttentionAsync(Guid accountId, AccountAttentionReason reason)
|
||||
{
|
||||
var accountMenuItem = MenuItems.GetAccountMenuItem(accountId);
|
||||
|
||||
if (accountMenuItem == null) return;
|
||||
if (!MenuItems.TryGetAccountMenuItem(accountId, out IAccountMenuItem accountMenuItem)) return;
|
||||
|
||||
var accountModel = accountMenuItem.HoldingAccounts.First(a => a.Id == accountId);
|
||||
|
||||
@@ -1025,7 +933,7 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
|
||||
public async void Receive(RefreshUnreadCountsMessage message)
|
||||
=> await RefreshUnreadCountsForAccountAsync(message.AccountId);
|
||||
=> await UpdateUnreadItemCountAsync();
|
||||
|
||||
public async void Receive(AccountsMenuRefreshRequested message)
|
||||
{
|
||||
@@ -1035,7 +943,7 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
|
||||
{
|
||||
ChangeLoadedAccount(firstAccount);
|
||||
await ChangeLoadedAccountAsync(firstAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1057,21 +965,48 @@ namespace Wino.Mail.ViewModels
|
||||
await CreateFooterItemsAsync();
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
ChangeLoadedAccount(latestSelectedAccountMenuItem, navigateInbox: false);
|
||||
await ChangeLoadedAccountAsync(latestSelectedAccountMenuItem, navigateInbox: false);
|
||||
}
|
||||
|
||||
private void ReorderAccountMenuItems(Dictionary<Guid, int> newAccountOrder)
|
||||
{
|
||||
foreach (var item in newAccountOrder)
|
||||
{
|
||||
var menuItem = MenuItems.GetAccountMenuItem(item.Key);
|
||||
|
||||
if (menuItem == null) continue;
|
||||
if (!MenuItems.TryGetAccountMenuItem(item.Key, out IAccountMenuItem menuItem)) return;
|
||||
|
||||
MenuItems.Move(MenuItems.IndexOf(menuItem), item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(AccountMenuItemsReordered message) => ReorderAccountMenuItems(message.newOrderDictionary);
|
||||
|
||||
private async void UpdateFolderCollection(IMailItemFolder updatedMailItemFolder)
|
||||
{
|
||||
var menuItem = MenuItems.GetAllFolderMenuItems(updatedMailItemFolder.Id);
|
||||
|
||||
if (!menuItem.Any()) return;
|
||||
|
||||
foreach (var item in menuItem)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
item.UpdateFolder(updatedMailItemFolder);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFolderRenamed(IMailItemFolder mailItemFolder)
|
||||
{
|
||||
base.OnFolderRenamed(mailItemFolder);
|
||||
|
||||
UpdateFolderCollection(mailItemFolder);
|
||||
}
|
||||
|
||||
protected override void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder)
|
||||
{
|
||||
base.OnFolderSynchronizationEnabled(mailItemFolder);
|
||||
|
||||
UpdateFolderCollection(mailItemFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Requests;
|
||||
@@ -15,16 +16,15 @@ namespace Wino.Mail.ViewModels
|
||||
IRecipient<AccountCreatedMessage>,
|
||||
IRecipient<AccountRemovedMessage>,
|
||||
IRecipient<AccountUpdatedMessage>,
|
||||
IRecipient<FolderAddedMessage>,
|
||||
IRecipient<FolderUpdatedMessage>,
|
||||
IRecipient<FolderRemovedMessage>,
|
||||
IRecipient<MailAddedMessage>,
|
||||
IRecipient<MailRemovedMessage>,
|
||||
IRecipient<MailUpdatedMessage>,
|
||||
IRecipient<MailDownloadedMessage>,
|
||||
IRecipient<DraftCreated>,
|
||||
IRecipient<DraftFailed>,
|
||||
IRecipient<DraftMapped>
|
||||
IRecipient<DraftMapped>,
|
||||
IRecipient<FolderRenamed>,
|
||||
IRecipient<FolderSynchronizationEnabled>
|
||||
{
|
||||
private IDispatcher _dispatcher;
|
||||
public IDispatcher Dispatcher
|
||||
@@ -65,13 +65,12 @@ namespace Wino.Mail.ViewModels
|
||||
protected virtual void OnAccountRemoved(MailAccount removedAccount) { }
|
||||
protected virtual void OnAccountUpdated(MailAccount updatedAccount) { }
|
||||
|
||||
protected virtual void OnFolderAdded(MailItemFolder addedFolder, MailAccount account) { }
|
||||
protected virtual void OnFolderRemoved(MailItemFolder removedFolder, MailAccount account) { }
|
||||
protected virtual void OnFolderUpdated(MailItemFolder updatedFolder, MailAccount account) { }
|
||||
|
||||
protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { }
|
||||
protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { }
|
||||
protected virtual void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId) { }
|
||||
protected virtual void OnFolderRenamed(IMailItemFolder mailItemFolder) { }
|
||||
protected virtual void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { }
|
||||
|
||||
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage
|
||||
=> Messenger.Send(message);
|
||||
@@ -80,16 +79,17 @@ namespace Wino.Mail.ViewModels
|
||||
void IRecipient<AccountRemovedMessage>.Receive(AccountRemovedMessage message) => OnAccountRemoved(message.Account);
|
||||
void IRecipient<AccountUpdatedMessage>.Receive(AccountUpdatedMessage message) => OnAccountUpdated(message.Account);
|
||||
|
||||
void IRecipient<FolderAddedMessage>.Receive(FolderAddedMessage message) => OnFolderAdded(message.AddedFolder, message.Account);
|
||||
void IRecipient<FolderUpdatedMessage>.Receive(FolderUpdatedMessage message) => OnFolderUpdated(message.UpdatedFolder, message.Account);
|
||||
void IRecipient<FolderRemovedMessage>.Receive(FolderRemovedMessage message) => OnFolderAdded(message.RemovedFolder, message.Account);
|
||||
|
||||
void IRecipient<MailAddedMessage>.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail);
|
||||
void IRecipient<MailRemovedMessage>.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail);
|
||||
void IRecipient<MailUpdatedMessage>.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail);
|
||||
void IRecipient<MailDownloadedMessage>.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail);
|
||||
|
||||
void IRecipient<DraftMapped>.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId);
|
||||
void IRecipient<DraftFailed>.Receive(DraftFailed message) => OnDraftFailed(message.DraftMail, message.Account);
|
||||
void IRecipient<DraftCreated>.Receive(DraftCreated message) => OnDraftCreated(message.DraftMail, message.Account);
|
||||
|
||||
void IRecipient<FolderRenamed>.Receive(FolderRenamed message) => OnFolderRenamed(message.MailItemFolder);
|
||||
void IRecipient<FolderSynchronizationEnabled>.Receive(FolderSynchronizationEnabled message) => OnFolderSynchronizationEnabled(message.MailItemFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
@@ -309,24 +310,24 @@ namespace Wino.Mail.ViewModels
|
||||
MailCollection.CoreDispatcher = Dispatcher;
|
||||
}
|
||||
|
||||
protected override async void OnFolderUpdated(MailItemFolder updatedFolder, MailAccount account)
|
||||
{
|
||||
base.OnFolderUpdated(updatedFolder, account);
|
||||
//protected override async void OnFolderUpdated(MailItemFolder updatedFolder, MailAccount account)
|
||||
//{
|
||||
// base.OnFolderUpdated(updatedFolder, account);
|
||||
|
||||
// Don't need to update if the folder update does not belong to the current folder menu item.
|
||||
if (ActiveFolder == null || updatedFolder == null || !ActiveFolder.HandlingFolders.Any(a => a.Id == updatedFolder.Id)) return;
|
||||
// // Don't need to update if the folder update does not belong to the current folder menu item.
|
||||
// if (ActiveFolder == null || updatedFolder == null || !ActiveFolder.HandlingFolders.Any(a => a.Id == updatedFolder.Id)) return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ActiveFolder.UpdateFolder(updatedFolder);
|
||||
// await ExecuteUIThread(() =>
|
||||
// {
|
||||
// ActiveFolder.UpdateFolder(updatedFolder);
|
||||
|
||||
OnPropertyChanged(nameof(CanSynchronize));
|
||||
OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
|
||||
});
|
||||
// OnPropertyChanged(nameof(CanSynchronize));
|
||||
// OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
|
||||
// });
|
||||
|
||||
// Force synchronization after enabling the folder.
|
||||
SyncFolder();
|
||||
}
|
||||
// // Force synchronization after enabling the folder.
|
||||
// SyncFolder();
|
||||
//}
|
||||
|
||||
private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message)
|
||||
{
|
||||
@@ -523,14 +524,6 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
await _folderService.ChangeFolderSynchronizationStateAsync(folder.Id, true);
|
||||
}
|
||||
|
||||
// TODO
|
||||
//ActiveFolder.IsSynchronizationEnabled = true;
|
||||
|
||||
//OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
|
||||
//OnPropertyChanged(nameof(CanSynchronize));
|
||||
|
||||
//SyncFolderCommand?.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -652,6 +645,8 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
base.OnMailRemoved(removedMail);
|
||||
|
||||
if (removedMail.AssignedAccount == null || removedMail.AssignedFolder == null) return;
|
||||
|
||||
// We should delete the items only if:
|
||||
// 1. They are deleted from the active folder.
|
||||
// 2. Deleted from draft or sent folder.
|
||||
@@ -954,6 +949,21 @@ namespace Wino.Mail.ViewModels
|
||||
public async void Receive(NewSynchronizationRequested message)
|
||||
=> await ExecuteUIThread(() => { OnPropertyChanged(nameof(CanSynchronize)); });
|
||||
|
||||
protected override async void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder)
|
||||
{
|
||||
if (ActiveFolder?.EntityId != mailItemFolder.Id) return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ActiveFolder.UpdateFolder(mailItemFolder);
|
||||
|
||||
OnPropertyChanged(nameof(CanSynchronize));
|
||||
OnPropertyChanged(nameof(IsFolderSynchronizationEnabled));
|
||||
});
|
||||
|
||||
SyncFolderCommand?.Execute(null);
|
||||
}
|
||||
|
||||
public async void Receive(AccountSynchronizerStateChanged message)
|
||||
=> await CheckIfAccountIsSynchronizingAsync();
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ using Wino.Core.Extensions;
|
||||
using Wino.Core.Messages.Mails;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class MailRenderingPageViewModel : BaseViewModel,
|
||||
IRecipient<NewMailItemRenderingRequestedEvent>,
|
||||
ITransferProgress // For listening IMAP message download progress.
|
||||
{
|
||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||
@@ -340,10 +342,6 @@ namespace Wino.Mail.ViewModels
|
||||
Crashes.TrackError(ex);
|
||||
Log.Error(ex, "Render Failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
StatePersistanceService.IsReadingMail = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -384,8 +382,7 @@ namespace Wino.Mail.ViewModels
|
||||
// Find the MIME for this item and render it.
|
||||
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
|
||||
mailItemViewModel.AssignedAccount.Id,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (mimeMessageInformation == null)
|
||||
{
|
||||
@@ -411,6 +408,8 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
Attachments.Clear();
|
||||
|
||||
Subject = message.Subject;
|
||||
|
||||
// TODO: FromName and FromAddress is probably not correct here for mail lists.
|
||||
@@ -447,6 +446,8 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsImageRenderingDisabled));
|
||||
|
||||
StatePersistanceService.IsReadingMail = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -467,10 +468,14 @@ namespace Wino.Mail.ViewModels
|
||||
BCCItems.Clear();
|
||||
Attachments.Clear();
|
||||
MenuItems.Clear();
|
||||
|
||||
StatePersistanceService.IsReadingMail = false;
|
||||
}
|
||||
|
||||
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)
|
||||
{
|
||||
collection.Clear();
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is MailboxAddress mailboxAddress)
|
||||
@@ -660,5 +665,7 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
// For upload.
|
||||
void ITransferProgress.Report(long bytesTransferred) { }
|
||||
|
||||
public async void Receive(NewMailItemRenderingRequestedEvent message) => await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
var newName = await DialogService.ShowTextInputDialogAsync(EditingMergedAccount.MergedInbox.Name,
|
||||
Translator.DialogMessage_RenameLinkedAccountsTitle,
|
||||
Translator.DialogMessage_RenameLinkedAccountsMessage);
|
||||
Translator.DialogMessage_RenameLinkedAccountsMessage,
|
||||
Translator.FolderOperation_Rename);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName)) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// When the rendering page is active, but new item is requested to be rendered.
|
||||
/// To not trigger navigation again and re-use existing Chromium.
|
||||
/// </summary>
|
||||
public class NewMailItemRenderingRequestedEvent
|
||||
{
|
||||
public NewMailItemRenderingRequestedEvent(MailItemViewModel mailItemViewModel)
|
||||
{
|
||||
MailItemViewModel = mailItemViewModel;
|
||||
}
|
||||
|
||||
public MailItemViewModel MailItemViewModel { get; }
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,15 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Border style for each page's root border for separation of zones. -->
|
||||
<Style TargetType="Border" x:Key="PageRootBorderStyle">
|
||||
<Setter Property="Margin" Value="7,0,7,7" />
|
||||
<Setter Property="Background" Value="{ThemeResource WinoContentZoneBackgroud}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Property="CornerRadius" Value="7" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
|
||||
<!-- Default StackPanel animation. -->
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="ChildrenTransitions">
|
||||
|
||||
@@ -30,95 +30,15 @@
|
||||
<muxc:NavigationViewItemSeparator Margin="-20,0" />
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Nested Account Template -->
|
||||
<DataTemplate x:Key="NestedAccountMenuTemplate" x:DataType="menu:AccountMenuItem">
|
||||
<controls:AccountNavigationItem
|
||||
x:Name="AccountItem"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
IsChildSelected="{x:Bind IsChildSelected, Mode=TwoWay}"
|
||||
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}"
|
||||
MenuItemsSource="{x:Bind SubMenuItems, Mode=OneWay}"
|
||||
SelectsOnInvoked="False">
|
||||
<controls:WinoNavigationViewItem.ContentTransitions>
|
||||
<TransitionCollection>
|
||||
<EdgeUIThemeTransition Edge="Top" />
|
||||
</TransitionCollection>
|
||||
</controls:WinoNavigationViewItem.ContentTransitions>
|
||||
<muxc:NavigationViewItem.Icon>
|
||||
<controls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Parameter.ProviderType)}" />
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
<muxc:NavigationViewItem.InfoBadge>
|
||||
<muxc:InfoBadge
|
||||
Background="{ThemeResource SystemAccentColor}"
|
||||
Foreground="White"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(UnreadItemCount), Mode=OneWay}"
|
||||
Value="{x:Bind UnreadItemCount, Mode=OneWay}" />
|
||||
</muxc:NavigationViewItem.InfoBadge>
|
||||
<Grid
|
||||
MaxHeight="70"
|
||||
Margin="0,8"
|
||||
RowSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
x:Name="AccountNameTextblock"
|
||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsChildSelected), Mode=OneWay}"
|
||||
MaxLines="1"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind AccountName, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<TextBlock
|
||||
FontSize="13"
|
||||
MaxLines="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Parameter.Address, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<PathIcon
|
||||
x:Name="AttentionIcon"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
x:Load="{x:Bind IsAttentionRequired, Mode=OneWay}"
|
||||
Data="F1 M 2.021484 18.769531 C 1.767578 18.769531 1.52832 18.720703 1.303711 18.623047 C 1.079102 18.525391 0.880534 18.391928 0.708008 18.222656 C 0.535482 18.053385 0.398763 17.856445 0.297852 17.631836 C 0.19694 17.407227 0.146484 17.167969 0.146484 16.914062 C 0.146484 16.614584 0.211589 16.328125 0.341797 16.054688 L 7.695312 1.347656 C 7.851562 1.035156 8.082682 0.784506 8.388672 0.595703 C 8.694661 0.406902 9.023438 0.3125 9.375 0.3125 C 9.726562 0.3125 10.055338 0.406902 10.361328 0.595703 C 10.667317 0.784506 10.898438 1.035156 11.054688 1.347656 L 18.408203 16.054688 C 18.53841 16.328125 18.603516 16.614584 18.603516 16.914062 C 18.603516 17.167969 18.553059 17.407227 18.452148 17.631836 C 18.351236 17.856445 18.216145 18.053385 18.046875 18.222656 C 17.877604 18.391928 17.679035 18.525391 17.451172 18.623047 C 17.223307 18.720703 16.982422 18.769531 16.728516 18.769531 Z M 16.728516 17.519531 C 16.884766 17.519531 17.027994 17.460938 17.158203 17.34375 C 17.28841 17.226562 17.353516 17.086588 17.353516 16.923828 C 17.353516 16.806641 17.330729 16.702475 17.285156 16.611328 L 9.931641 1.904297 C 9.879557 1.793621 9.80306 1.708984 9.702148 1.650391 C 9.601236 1.591797 9.492188 1.5625 9.375 1.5625 C 9.257812 1.5625 9.148763 1.593426 9.047852 1.655273 C 8.946939 1.717123 8.870442 1.800131 8.818359 1.904297 L 1.464844 16.611328 C 1.419271 16.702475 1.396484 16.803387 1.396484 16.914062 C 1.396484 17.083334 1.459961 17.226562 1.586914 17.34375 C 1.713867 17.460938 1.858724 17.519531 2.021484 17.519531 Z M 8.75 11.875 L 8.75 6.875 C 8.75 6.705729 8.811849 6.559245 8.935547 6.435547 C 9.059244 6.31185 9.205729 6.25 9.375 6.25 C 9.544271 6.25 9.690755 6.31185 9.814453 6.435547 C 9.93815 6.559245 10 6.705729 10 6.875 L 10 11.875 C 10 12.044271 9.93815 12.190756 9.814453 12.314453 C 9.690755 12.438151 9.544271 12.5 9.375 12.5 C 9.205729 12.5 9.059244 12.438151 8.935547 12.314453 C 8.811849 12.190756 8.75 12.044271 8.75 11.875 Z M 8.4375 14.375 C 8.4375 14.114584 8.528646 13.893229 8.710938 13.710938 C 8.893229 13.528646 9.114583 13.4375 9.375 13.4375 C 9.635416 13.4375 9.856771 13.528646 10.039062 13.710938 C 10.221354 13.893229 10.3125 14.114584 10.3125 14.375 C 10.3125 14.635417 10.221354 14.856771 10.039062 15.039062 C 9.856771 15.221354 9.635416 15.3125 9.375 15.3125 C 9.114583 15.3125 8.893229 15.221354 8.710938 15.039062 C 8.528646 14.856771 8.4375 14.635417 8.4375 14.375 Z "
|
||||
Foreground="{ThemeResource InfoBarWarningSeverityIconBackground}" />
|
||||
|
||||
<muxc:ProgressBar
|
||||
x:Name="SynchronizationProgressBar"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{ThemeResource AppBarItemBackgroundThemeBrush}"
|
||||
Foreground="{ThemeResource AppBarItemForegroundThemeBrush}"
|
||||
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
|
||||
Value="{x:Bind SynchronizationProgress, Mode=OneWay}" />
|
||||
|
||||
</Grid>
|
||||
</controls:AccountNavigationItem>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Clickable New Style Account Template -->
|
||||
<DataTemplate x:Key="ClickableAccountMenuTemplate" x:DataType="menu:AccountMenuItem">
|
||||
<controls:AccountNavigationItem
|
||||
x:Name="AccountItem"
|
||||
BindingData="{x:Bind}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
DataContext="{x:Bind}"
|
||||
IsActiveAccount="{x:Bind IsSelected, Mode=OneWay}"
|
||||
IsChildSelected="{x:Bind IsChildSelected, Mode=TwoWay}"
|
||||
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}"
|
||||
SelectsOnInvoked="False"
|
||||
Style="{StaticResource SingleAccountNavigationViewItemTemplate}">
|
||||
@@ -128,7 +48,7 @@
|
||||
</TransitionCollection>
|
||||
</controls:WinoNavigationViewItem.ContentTransitions>
|
||||
<muxc:NavigationViewItem.Icon>
|
||||
<controls:WinoFontIcon FontSize="64" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Parameter.ProviderType)}" />
|
||||
<controls:WinoFontIcon FontSize="12" Icon="{x:Bind helpers:XamlHelpers.GetProviderIcon(Parameter.ProviderType)}" />
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
<muxc:NavigationViewItem.InfoBadge>
|
||||
<muxc:InfoBadge
|
||||
@@ -326,14 +246,15 @@
|
||||
<DataTemplate x:Key="MergedAccountTemplate" x:DataType="menu:MergedAccountMenuItem">
|
||||
<controls:AccountNavigationItem
|
||||
x:Name="AccountItem"
|
||||
BindingData="{x:Bind}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
DataContext="{x:Bind}"
|
||||
IsActiveAccount="{x:Bind IsSelected, Mode=OneWay}"
|
||||
IsChildSelected="{x:Bind IsChildSelected, Mode=TwoWay}"
|
||||
IsActiveAccount="{x:Bind IsSelected, Mode=TwoWay}"
|
||||
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}"
|
||||
SelectsOnInvoked="False"
|
||||
Style="{StaticResource SingleAccountNavigationViewItemTemplate}">
|
||||
MenuItemsSource="{x:Bind SubMenuItems}"
|
||||
Style="{StaticResource SingleAccountNavigationViewItemTemplate}"
|
||||
SelectsOnInvoked="False">
|
||||
<muxc:NavigationViewItem.InfoBadge>
|
||||
<muxc:InfoBadge
|
||||
x:Name="FolderInfoBadge"
|
||||
@@ -347,9 +268,9 @@
|
||||
<EdgeUIThemeTransition Edge="Top" />
|
||||
</TransitionCollection>
|
||||
</controls:WinoNavigationViewItem.ContentTransitions>
|
||||
<controls:AccountNavigationItem.Icon>
|
||||
<controls:WinoNavigationViewItem.Icon>
|
||||
<PathIcon Data="F1 M 8.613281 17.5 C 8.75 17.942709 8.945312 18.359375 9.199219 18.75 L 4.921875 18.75 C 4.433594 18.75 3.966471 18.650717 3.520508 18.452148 C 3.074544 18.25358 2.683919 17.986654 2.348633 17.651367 C 2.013346 17.31608 1.746419 16.925455 1.547852 16.479492 C 1.349284 16.033529 1.25 15.566406 1.25 15.078125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 L 15.078125 1.25 C 15.566406 1.25 16.033527 1.349285 16.479492 1.547852 C 16.925455 1.74642 17.31608 2.013348 17.651367 2.348633 C 17.986652 2.68392 18.25358 3.074545 18.452148 3.520508 C 18.650715 3.966473 18.75 4.433594 18.75 4.921875 L 18.75 6.572266 C 18.580729 6.344402 18.390299 6.132813 18.178711 5.9375 C 17.967121 5.742188 17.740885 5.566407 17.5 5.410156 L 17.5 4.951172 C 17.5 4.625651 17.433268 4.314779 17.299805 4.018555 C 17.16634 3.722332 16.987305 3.461914 16.762695 3.237305 C 16.538086 3.012695 16.277668 2.83366 15.981445 2.700195 C 15.685221 2.566732 15.374349 2.5 15.048828 2.5 L 4.951172 2.5 C 4.619141 2.5 4.303385 2.568359 4.003906 2.705078 C 3.704427 2.841797 3.44401 3.02409 3.222656 3.251953 C 3.001302 3.479818 2.825521 3.745117 2.695312 4.047852 C 2.565104 4.350587 2.5 4.66797 2.5 5 L 13.310547 5 C 12.60091 5.266928 11.998697 5.683594 11.503906 6.25 L 2.5 6.25 L 2.5 15.048828 C 2.5 15.38737 2.568359 15.704753 2.705078 16.000977 C 2.841797 16.297201 3.024088 16.55599 3.251953 16.777344 C 3.479818 16.998697 3.745117 17.174479 4.047852 17.304688 C 4.350586 17.434896 4.667969 17.5 5 17.5 Z M 18.125 9.443359 C 18.125 9.866537 18.040363 10.263672 17.871094 10.634766 C 17.701822 11.005859 17.473957 11.329753 17.1875 11.606445 C 16.901041 11.883139 16.56901 12.101237 16.191406 12.260742 C 15.813802 12.420248 15.416666 12.5 15 12.5 C 14.563802 12.5 14.1569 12.41862 13.779297 12.255859 C 13.401691 12.0931 13.071288 11.870117 12.788086 11.586914 C 12.504882 11.303711 12.2819 10.973308 12.119141 10.595703 C 11.95638 10.2181 11.875 9.811198 11.875 9.375 C 11.875 8.938803 11.95638 8.531901 12.119141 8.154297 C 12.2819 7.776693 12.504882 7.446289 12.788086 7.163086 C 13.071288 6.879883 13.401691 6.656901 13.779297 6.494141 C 14.1569 6.331381 14.563802 6.25 15 6.25 C 15.449218 6.25 15.864257 6.333008 16.245117 6.499023 C 16.625977 6.665039 16.956379 6.892904 17.236328 7.182617 C 17.516275 7.472331 17.734375 7.810873 17.890625 8.198242 C 18.046875 8.585612 18.125 9.000651 18.125 9.443359 Z M 20 16.25 C 20 16.666666 19.926758 17.049154 19.780273 17.397461 C 19.633789 17.745768 19.435221 18.058268 19.18457 18.334961 C 18.933918 18.611654 18.642578 18.854166 18.310547 19.0625 C 17.978516 19.270834 17.626953 19.444986 17.255859 19.584961 C 16.884766 19.724936 16.505533 19.829102 16.118164 19.897461 C 15.730794 19.96582 15.358072 20 15 20 C 14.654947 20 14.291992 19.96582 13.911133 19.897461 C 13.530273 19.829102 13.154297 19.726562 12.783203 19.589844 C 12.412109 19.453125 12.058919 19.282227 11.723633 19.077148 C 11.388346 18.87207 11.092122 18.632812 10.834961 18.359375 C 10.577799 18.085938 10.374349 17.779947 10.224609 17.441406 C 10.074869 17.102865 10 16.731771 10 16.328125 L 10 15.78125 C 10 15.501303 10.052083 15.237631 10.15625 14.990234 C 10.260416 14.742839 10.405273 14.526367 10.59082 14.34082 C 10.776367 14.155273 10.991211 14.010417 11.235352 13.90625 C 11.479492 13.802084 11.744791 13.75 12.03125 13.75 L 17.96875 13.75 C 18.248697 13.75 18.512369 13.803711 18.759766 13.911133 C 19.00716 14.018555 19.222004 14.163412 19.404297 14.345703 C 19.586588 14.527995 19.731445 14.742839 19.838867 14.990234 C 19.946289 15.237631 20 15.501303 20 15.78125 Z " />
|
||||
</controls:AccountNavigationItem.Icon>
|
||||
</controls:WinoNavigationViewItem.Icon>
|
||||
|
||||
<Grid MinHeight="50">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="0">
|
||||
@@ -480,7 +401,6 @@
|
||||
MergedAccountFolderTemplate="{StaticResource MergedAccountFolderMenuItemTemplate}"
|
||||
MergedAccountMoreExpansionItemTemplate="{StaticResource MergedAccountMoreFolderItemTemplate}"
|
||||
MergedAccountTemplate="{StaticResource MergedAccountTemplate}"
|
||||
NestedAccountMenuTemplate="{StaticResource NestedAccountMenuTemplate}"
|
||||
NewMailTemplate="{StaticResource CreateNewMailTemplate}"
|
||||
RatingItemTemplate="{StaticResource RatingItemTemplate}"
|
||||
SeperatorTemplate="{StaticResource SeperatorTemplate}"
|
||||
|
||||
@@ -140,20 +140,21 @@ namespace Wino.Views
|
||||
{
|
||||
if (message.FolderId == default) return;
|
||||
|
||||
var menuItem = ViewModel.MenuItems.GetFolderItem(message.FolderId);
|
||||
if (ViewModel.MenuItems.TryGetFolderMenuItem(message.FolderId, out IBaseFolderMenuItem foundMenuItem))
|
||||
{
|
||||
if (foundMenuItem == null) return;
|
||||
|
||||
if (menuItem == null) return;
|
||||
foundMenuItem.Expand();
|
||||
|
||||
menuItem.Expand();
|
||||
await ViewModel.NavigateFolderAsync(foundMenuItem);
|
||||
|
||||
await ViewModel.NavigateFolderAsync(menuItem);
|
||||
navigationView.SelectedItem = foundMenuItem;
|
||||
|
||||
navigationView.SelectedItem = menuItem;
|
||||
if (message.NavigateMailItem == null) return;
|
||||
|
||||
if (message.NavigateMailItem == null) return;
|
||||
|
||||
// At this point folder is navigated and items are loaded.
|
||||
WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId));
|
||||
// At this point folder is navigated and items are loaded.
|
||||
WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,6 +206,9 @@ namespace Wino.Views
|
||||
private void BackButtonClicked(Controls.Advanced.WinoAppTitleBar sender, RoutedEventArgs args)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
|
||||
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
|
||||
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
|
||||
|
||||
}
|
||||
|
||||
private async void MenuItemContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using CommunityToolkit.WinUI;
|
||||
using System.Numerics;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Windows.UI.Xaml;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Controls
|
||||
{
|
||||
public class AccountNavigationItem : WinoNavigationViewItem
|
||||
{
|
||||
|
||||
public static readonly DependencyProperty IsActiveAccountProperty = DependencyProperty.Register(nameof(IsActiveAccount), typeof(bool), typeof(AccountNavigationItem), new PropertyMetadata(false, new PropertyChangedCallback(OnIsActiveAccountChanged)));
|
||||
public static readonly DependencyProperty BindingDataProperty = DependencyProperty.Register(nameof(BindingData), typeof(IAccountMenuItem), typeof(AccountNavigationItem), new PropertyMetadata(null));
|
||||
|
||||
|
||||
public bool IsActiveAccount
|
||||
{
|
||||
get { return (bool)GetValue(IsActiveAccountProperty); }
|
||||
set { SetValue(IsActiveAccountProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsActiveAccountProperty = DependencyProperty.Register(nameof(IsActiveAccount), typeof(bool), typeof(AccountNavigationItem), new PropertyMetadata(false, new PropertyChangedCallback(OnIsActiveAccountChanged)));
|
||||
|
||||
public IAccountMenuItem BindingData
|
||||
{
|
||||
get { return (IAccountMenuItem)GetValue(BindingDataProperty); }
|
||||
set { SetValue(BindingDataProperty, value); }
|
||||
}
|
||||
|
||||
private const string PART_NavigationViewItemMenuItemsHost = "NavigationViewItemMenuItemsHost";
|
||||
private const string PART_SelectionIndicator = "SelectionIndicator";
|
||||
private const string PART_SelectionIndicator = "CustomSelectionIndicator";
|
||||
|
||||
private ItemsRepeater _itemsRepeater;
|
||||
private Windows.UI.Xaml.Shapes.Rectangle _selectionIndicator;
|
||||
|
||||
public AccountNavigationItem()
|
||||
{
|
||||
DefaultStyleKey = typeof(AccountNavigationItem);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
@@ -48,7 +59,13 @@ namespace Wino.Controls
|
||||
{
|
||||
if (_selectionIndicator == null) return;
|
||||
|
||||
_selectionIndicator.Scale = IsActiveAccount ? new Vector3(1,1,1) : new Vector3(0,0,0);
|
||||
// Adjsuting Margin in the styles are not possible due to the fact that we use the same tempalte for different types of menu items.
|
||||
// Account templates listed under merged accounts will have Padding of 44. We must adopt to that.
|
||||
|
||||
bool hasParentMenuItem = BindingData is IAccountMenuItem accountMenuItem && accountMenuItem.ParentMenuItem != null;
|
||||
|
||||
_selectionIndicator.Margin = !hasParentMenuItem ? new Thickness(-44, 12, 0, 12) : new Thickness(-60, 12, -60, 12);
|
||||
_selectionIndicator.Scale = IsActiveAccount ? new Vector3(1, 1, 1) : new Vector3(0, 0, 0);
|
||||
_selectionIndicator.Visibility = IsActiveAccount ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<StackPanel
|
||||
x:Name="LeftMenuStackPanel"
|
||||
Orientation="Horizontal"
|
||||
SizeChanged="asd">
|
||||
SizeChanged="TitlebarSizeChanged">
|
||||
<Button
|
||||
x:Name="PaneButton"
|
||||
Width="48"
|
||||
|
||||
@@ -161,9 +161,6 @@ namespace Wino.Controls.Advanced
|
||||
IsNavigationPaneOpen = !IsNavigationPaneOpen;
|
||||
}
|
||||
|
||||
private void asd(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
DrawTitleBar();
|
||||
}
|
||||
private void TitlebarSizeChanged(object sender, SizeChangedEventArgs e) => DrawTitleBar();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
Style="{StaticResource WinoDialogStyle}"
|
||||
DefaultButton="Primary"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Create}"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
PrimaryButtonClick="UpdateOrCreateClicked"
|
||||
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace Wino.Dialogs
|
||||
DialogDescription.Text = description;
|
||||
}
|
||||
|
||||
public void SetPrimaryButtonText(string text)
|
||||
{
|
||||
PrimaryButtonText = text;
|
||||
}
|
||||
|
||||
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
Hide();
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace Wino.Selectors
|
||||
{
|
||||
public DataTemplate MenuItemTemplate { get; set; }
|
||||
public DataTemplate AccountManagementTemplate { get; set; }
|
||||
public DataTemplate NestedAccountMenuTemplate { get; set; }
|
||||
public DataTemplate ClickableAccountMenuTemplate { get; set; }
|
||||
public DataTemplate MergedAccountTemplate { get; set; }
|
||||
public DataTemplate MergedAccountFolderTemplate { get; set; }
|
||||
@@ -34,7 +33,7 @@ namespace Wino.Selectors
|
||||
return SeperatorTemplate;
|
||||
else if (item is AccountMenuItem accountMenuItem)
|
||||
// Merged inbox account menu items must be nested.
|
||||
return accountMenuItem.Parameter.MergedInboxId != null ? NestedAccountMenuTemplate : ClickableAccountMenuTemplate;
|
||||
return ClickableAccountMenuTemplate;
|
||||
else if (item is ManageAccountsMenuItem)
|
||||
return AccountManagementTemplate;
|
||||
else if (item is RateMenuItem)
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Wino.Services
|
||||
public void InfoBarMessage(string title, string message, InfoBarMessageType messageType, string actionButtonText, Action action)
|
||||
=> WeakReferenceMessenger.Default.Send(new InfoBarMessageRequested(messageType, title, message, actionButtonText, action));
|
||||
|
||||
public async Task<string> ShowTextInputDialogAsync(string currentInput, string dialogTitle, string dialogDescription)
|
||||
public async Task<string> ShowTextInputDialogAsync(string currentInput, string dialogTitle, string dialogDescription, string primaryButtonText)
|
||||
{
|
||||
var inputDialog = new TextInputDialog()
|
||||
{
|
||||
@@ -161,6 +161,7 @@ namespace Wino.Services
|
||||
};
|
||||
|
||||
inputDialog.SetDescription(dialogDescription);
|
||||
inputDialog.SetPrimaryButtonText(primaryButtonText);
|
||||
|
||||
await HandleDialogPresentationAsync(inputDialog);
|
||||
|
||||
|
||||
@@ -125,7 +125,19 @@ namespace Wino.Services
|
||||
|
||||
if (listingFrame == null) return false;
|
||||
|
||||
listingFrame.Navigate(pageType, parameter, transitionInfo);
|
||||
// Active page is mail list page and we are opening a mail item.
|
||||
// No navigation needed, just refresh the rendered mail item.
|
||||
if (listingFrame.Content != null
|
||||
&& listingFrame.Content.GetType() == GetPageType(WinoPage.MailRenderingPage)
|
||||
&& parameter is MailItemViewModel mailItemViewModel
|
||||
&& page != WinoPage.ComposePage)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
|
||||
}
|
||||
else
|
||||
{
|
||||
listingFrame.Navigate(pageType, parameter, transitionInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -169,10 +181,6 @@ namespace Wino.Services
|
||||
throw new ArgumentException("MailItem must be of type MailItemViewModel.");
|
||||
}
|
||||
|
||||
public void NavigateWelcomePage() => Navigate(WinoPage.WelcomePage);
|
||||
|
||||
public void NavigateManageAccounts() => Navigate(WinoPage.AccountManagementPage);
|
||||
|
||||
public void NavigateFolder(NavigateMailFolderEventArgs args)
|
||||
=> Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.ShellFrame);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:primitiveContract7Present="using:Microsoft.UI.Xaml.Controls.Primitives?IsApiContractPresent(Windows.Foundation.UniversalApiContract,7)"
|
||||
xmlns:primitives="using:Microsoft.UI.Xaml.Controls.Primitives"
|
||||
xmlns:winoControls="using:Wino.Controls"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
@@ -149,7 +150,7 @@
|
||||
<!-- Clickable account navigation view item style -->
|
||||
<!-- This introduces custom selector pipe for multi selection in NavigationView in shell. -->
|
||||
|
||||
<Style x:Key="SingleAccountNavigationViewItemTemplate" TargetType="muxc:NavigationViewItem">
|
||||
<Style x:Key="SingleAccountNavigationViewItemTemplate" TargetType="winoControls:AccountNavigationItem">
|
||||
<Setter Property="Foreground" Value="{ThemeResource NavigationViewItemForeground}" />
|
||||
<Setter Property="Background" Value="{ThemeResource NavigationViewItemBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource NavigationViewItemBorderBrush}" />
|
||||
@@ -163,46 +164,105 @@
|
||||
<Setter Property="TabNavigation" Value="Once" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="muxc:NavigationViewItem">
|
||||
<ControlTemplate TargetType="winoControls:AccountNavigationItem">
|
||||
<Grid x:Name="NVIRootGrid">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<primitives:NavigationViewItemPresenter
|
||||
x:Name="NavigationViewItemPresenter"
|
||||
Icon="{TemplateBinding Icon}"
|
||||
InfoBadge="{TemplateBinding InfoBadge}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
UseSystemFocusVisuals="{TemplateBinding UseSystemFocusVisuals}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
|
||||
CornerRadius="4"
|
||||
IsTabStop="false"
|
||||
Control.IsTemplateFocusTarget="True">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
x:Name="CustomSelectionIndicator"
|
||||
Width="3"
|
||||
Opacity="1"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="Collapsed"
|
||||
Scale="0,0,0"
|
||||
Fill="{ThemeResource NavigationViewSelectionIndicatorForeground}"
|
||||
RadiusX="2"
|
||||
RadiusY="2">
|
||||
<Rectangle.ScaleTransition>
|
||||
<Vector3Transition />
|
||||
</Rectangle.ScaleTransition>
|
||||
</Rectangle>
|
||||
<ContentPresenter Content="{TemplateBinding Content}" Grid.Column="1" />
|
||||
</Grid>
|
||||
</primitives:NavigationViewItemPresenter>
|
||||
|
||||
<muxc:ItemsRepeater
|
||||
x:Load="False"
|
||||
Grid.Row="1"
|
||||
Visibility="Collapsed"
|
||||
x:Name="NavigationViewItemMenuItemsHost">
|
||||
<muxc:ItemsRepeater.Layout>
|
||||
<muxc:StackLayout Orientation="Vertical" />
|
||||
</muxc:ItemsRepeater.Layout>
|
||||
</muxc:ItemsRepeater>
|
||||
|
||||
|
||||
<!-- Custom selecotr pipe. -->
|
||||
|
||||
|
||||
<FlyoutBase.AttachedFlyout>
|
||||
<Flyout x:Name="ChildrenFlyout" Placement="Right">
|
||||
<Flyout x:Name="ChildrenFlyout" Placement="RightEdgeAlignedTop">
|
||||
<Flyout.FlyoutPresenterStyle>
|
||||
<Style TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="{ThemeResource NavigationViewItemChildrenMenuFlyoutPadding}" />
|
||||
<!-- Set negative top margin to make the flyout align exactly with the button -->
|
||||
<Setter Property="Margin" Value="0,-4,0,0" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Auto" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollMode" Value="Auto" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="ScrollViewer.ZoomMode" Value="Disabled" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource OverlayCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="FlyoutPresenter">
|
||||
<ScrollViewer
|
||||
x:Name="ScrollViewer"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
|
||||
ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"
|
||||
HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
|
||||
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
|
||||
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
|
||||
VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
|
||||
ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}">
|
||||
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
|
||||
AutomationProperties.AccessibilityView="Raw">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
CornerRadius="{ThemeResource OverlayCornerRadius}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
@@ -214,53 +274,6 @@
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</FlyoutBase.AttachedFlyout>
|
||||
<primitiveContract7Present:NavigationViewItemPresenter
|
||||
x:Name="NavigationViewItemPresenter"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
IsTabStop="false"
|
||||
Icon="{TemplateBinding Icon}"
|
||||
InfoBadge="{TemplateBinding InfoBadge}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
UseSystemFocusVisuals="{TemplateBinding UseSystemFocusVisuals}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}" />
|
||||
<muxc:ItemsRepeater
|
||||
x:Name="NavigationViewItemMenuItemsHost"
|
||||
Grid.Row="1"
|
||||
Visibility="Collapsed"
|
||||
x:Load="False">
|
||||
<muxc:ItemsRepeater.Layout>
|
||||
<muxc:StackLayout Orientation="Vertical" />
|
||||
</muxc:ItemsRepeater.Layout>
|
||||
</muxc:ItemsRepeater>
|
||||
|
||||
<!-- Custom selecotr pipe. -->
|
||||
<Rectangle
|
||||
x:Name="SelectionIndicator"
|
||||
Width="3"
|
||||
Opacity="1"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="Collapsed"
|
||||
Margin="4,12"
|
||||
Scale="0,0,0"
|
||||
Fill="{ThemeResource NavigationViewSelectionIndicatorForeground}"
|
||||
RadiusX="2"
|
||||
RadiusY="2">
|
||||
<Rectangle.ScaleTransition>
|
||||
<Vector3Transition />
|
||||
</Rectangle.ScaleTransition>
|
||||
</Rectangle>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ItemOnNavigationViewListPositionStates">
|
||||
<VisualState x:Name="OnLeftNavigation">
|
||||
@@ -272,7 +285,7 @@
|
||||
<VisualState.Setters>
|
||||
<Setter Target="NavigationViewItemPresenter.Margin" Value="{ThemeResource TopNavigationViewItemMargin}" />
|
||||
<Setter Target="NavigationViewItemPresenter.Style" Value="{StaticResource MUX_NavigationViewItemPresenterStyleWhenOnTopPane}" />
|
||||
<Setter Target="ChildrenFlyout.Placement" Value="Bottom" />
|
||||
<Setter Target="ChildrenFlyout.Placement" Value="BottomEdgeAlignedLeft" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="OnTopNavigationOverflow">
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical">
|
||||
<Grid Height="160" RowSpacing="6">
|
||||
<Grid MinHeight="160" RowSpacing="6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
|
||||
@@ -811,7 +811,7 @@
|
||||
|
||||
<Grid Grid.Column="1" x:Name="RenderingGrid">
|
||||
<!-- Mail Rendering Frame -->
|
||||
<Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" />
|
||||
<Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" />
|
||||
|
||||
<!-- No Mail Selected Message -->
|
||||
<StackPanel
|
||||
|
||||
@@ -37,7 +37,8 @@ namespace Wino.Views
|
||||
IRecipient<ActiveMailItemChangedEvent>,
|
||||
IRecipient<ActiveMailFolderChangedEvent>,
|
||||
IRecipient<SelectMailItemContainerEvent>,
|
||||
IRecipient<ShellStateUpdated>
|
||||
IRecipient<ShellStateUpdated>,
|
||||
IRecipient<DisposeRenderingFrameRequested>
|
||||
{
|
||||
private const string NarrowVisualStateKey = "NarrowState";
|
||||
private const string AdaptivenessStatesKey = "AdaptiveStates";
|
||||
@@ -562,5 +563,10 @@ namespace Wino.Views
|
||||
await ViewModel.PerformSearchAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(DisposeRenderingFrameRequested message)
|
||||
{
|
||||
ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.DrillIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Wino.Views
|
||||
InitializeComponent();
|
||||
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00FFFFFF");
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--enable-features=OverlayScrollbar,msOverlayScrollbarWinStyle,msOverlayScrollbarWinStyleAnimation");
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--enable-features=OverlayScrollbar,msOverlayScrollbarWinStyle,msOverlayScrollbarWinStyleAnimation,msWebView2CodeCache");
|
||||
}
|
||||
|
||||
public override async void OnEditorThemeChanged()
|
||||
|
||||
@@ -12,11 +12,7 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border
|
||||
Margin="0,0,7,7"
|
||||
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7">
|
||||
Style="{StaticResource PageRootBorderStyle}">
|
||||
<Grid
|
||||
MaxWidth="900"
|
||||
Padding="20"
|
||||
|
||||
@@ -11,12 +11,7 @@
|
||||
Style="{StaticResource PageStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Border
|
||||
Margin="0,0,7,7"
|
||||
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7">
|
||||
<Border Style="{StaticResource PageRootBorderStyle}">
|
||||
<Grid
|
||||
MaxWidth="900"
|
||||
Padding="20"
|
||||
|
||||
Reference in New Issue
Block a user