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:
Burak Kaan Köse
2024-07-09 01:05:16 +02:00
committed by GitHub
parent ac01006398
commit 536fbb23a1
67 changed files with 1396 additions and 1585 deletions

View File

@@ -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;
}
}

View File

@@ -11,8 +11,10 @@
ChangeFlag,
AlwaysMoveTo,
MoveToFocused,
Archive,
RenameFolder,
Archive
EmptyFolder,
MarkFolderRead,
}
// UI requests

View File

@@ -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; }
}
}

View File

@@ -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);

View File

@@ -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; }

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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; }
}
}

View File

@@ -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) { }
}

View File

@@ -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; }
}
}

View File

@@ -3,6 +3,6 @@
public enum NavigationTransitionType
{
None, // Supress
DrillIn,
DrillIn
}
}

View File

@@ -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;
}
}

View File

@@ -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.",

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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);
{
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Wino.Core.Messages.Mails
{
/// <summary>
/// When rendering frame should be disposed.
/// </summary>
public class DisposeRenderingFrameRequested { }
}

View 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];
}
}

View 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];
}
}

View File

@@ -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));
}
}
}

View File

@@ -55,5 +55,7 @@ namespace Wino.Core.Requests
{
Items.ForEach(item => WeakReferenceMessenger.Default.Send(new MailAddedMessage(item.Item)));
}
public override bool DelayExecution => true;
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}
}

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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.");

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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; }
}
}

View File

@@ -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">

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -32,7 +32,7 @@
<StackPanel
x:Name="LeftMenuStackPanel"
Orientation="Horizontal"
SizeChanged="asd">
SizeChanged="TitlebarSizeChanged">
<Button
x:Name="PaneButton"
Width="48"

View File

@@ -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();
}
}

View File

@@ -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}"

View File

@@ -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();

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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">

View File

@@ -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" />

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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()

View File

@@ -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"

View File

@@ -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"