Merge branch 'codex/mail-categories-v1'
This commit is contained in:
@@ -49,6 +49,8 @@ public class DatabaseService : IDatabaseService
|
||||
{
|
||||
await Task.WhenAll(
|
||||
Connection.CreateTableAsync<MailCopy>(),
|
||||
Connection.CreateTableAsync<MailCategory>(),
|
||||
Connection.CreateTableAsync<MailCategoryAssignment>(),
|
||||
Connection.CreateTableAsync<MailItemFolder>(),
|
||||
Connection.CreateTableAsync<MailAccount>(),
|
||||
Connection.CreateTableAsync<AccountContact>(),
|
||||
@@ -226,6 +228,12 @@ SET {nameof(KeyboardShortcut.Action)} =
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_MessageId ON MailCopy(MessageId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_FolderId_IsRead ON MailCopy(FolderId, IsRead)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_CreationDate ON MailCopy(CreationDate)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId ON MailCategory(MailAccountId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId_Name ON MailCategory(MailAccountId, Name)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId_IsFavorite ON MailCategory(MailAccountId, IsFavorite)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategoryAssignment_MailCategoryId ON MailCategoryAssignment(MailCategoryId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategoryAssignment_MailCopyUniqueId ON MailCategoryAssignment(MailCopyUniqueId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailCategoryAssignment_Category_MailCopy ON MailCategoryAssignment(MailCategoryId, MailCopyUniqueId)").ConfigureAwait(false);
|
||||
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId ON MailItemFolder(MailAccountId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId_RemoteFolderId ON MailItemFolder(MailAccountId, RemoteFolderId)").ConfigureAwait(false);
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Wino.Services;
|
||||
public class FolderService : BaseDatabaseService, IFolderService
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IMailCategoryService _mailCategoryService;
|
||||
private readonly ILogger _logger = Log.ForContext<FolderService>();
|
||||
|
||||
private readonly SpecialFolderType[] gmailCategoryFolderTypes =
|
||||
@@ -34,9 +35,11 @@ public class FolderService : BaseDatabaseService, IFolderService
|
||||
];
|
||||
|
||||
public FolderService(IDatabaseService databaseService,
|
||||
IAccountService accountService) : base(databaseService)
|
||||
IAccountService accountService,
|
||||
IMailCategoryService mailCategoryService) : base(databaseService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_mailCategoryService = mailCategoryService;
|
||||
}
|
||||
|
||||
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
|
||||
@@ -269,6 +272,9 @@ public class FolderService : BaseDatabaseService, IFolderService
|
||||
}
|
||||
}
|
||||
|
||||
var favoriteCategories = await GetFavoriteCategoryMenuItemsAsync(mailAccount, folders, accountMenuItem).ConfigureAwait(false);
|
||||
preparedFolderMenuItems.AddRange(favoriteCategories);
|
||||
|
||||
// Only add category folder if it's Gmail.
|
||||
if (mailAccount.ProviderType == MailProviderType.Gmail) preparedFolderMenuItems.Add(categoryFolderMenuItem);
|
||||
|
||||
@@ -309,9 +315,62 @@ public class FolderService : BaseDatabaseService, IFolderService
|
||||
preparedFolderMenuItems.Add(menuItem);
|
||||
}
|
||||
|
||||
var favoriteCategories = await GetMergedFavoriteCategoryMenuItemsAsync(holdingAccounts, allAccountFolders, mergedAccountFolderMenuItem.Parameter).ConfigureAwait(false);
|
||||
preparedFolderMenuItems.AddRange(favoriteCategories);
|
||||
|
||||
return preparedFolderMenuItems;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<IMenuItem>> GetFavoriteCategoryMenuItemsAsync(MailAccount account, IEnumerable<IMailItemFolder> handlingFolders, IMenuItem parentMenuItem)
|
||||
{
|
||||
var favoriteCategories = await _mailCategoryService.GetFavoriteCategoriesAsync(account.Id).ConfigureAwait(false);
|
||||
|
||||
if (!favoriteCategories.Any())
|
||||
return [];
|
||||
|
||||
var availableFolders = handlingFolders
|
||||
.Where(a => a.IsMoveTarget)
|
||||
.Cast<IMailItemFolder>()
|
||||
.ToList();
|
||||
|
||||
return favoriteCategories
|
||||
.Select(category => (IMenuItem)new MailCategoryMenuItem(category, account, availableFolders, parentMenuItem))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<IMenuItem>> GetMergedFavoriteCategoryMenuItemsAsync(IEnumerable<MailAccount> holdingAccounts, IEnumerable<IEnumerable<MailItemFolder>> allAccountFolders, MergedInbox mergedInbox)
|
||||
{
|
||||
var categoriesByAccount = new List<(MailAccount Account, List<MailCategory> Categories)>();
|
||||
|
||||
foreach (var account in holdingAccounts)
|
||||
{
|
||||
var categories = await _mailCategoryService.GetFavoriteCategoriesAsync(account.Id).ConfigureAwait(false);
|
||||
if (categories.Any())
|
||||
{
|
||||
categoriesByAccount.Add((account, categories));
|
||||
}
|
||||
}
|
||||
|
||||
if (!categoriesByAccount.Any())
|
||||
return [];
|
||||
|
||||
var handlingFolders = allAccountFolders
|
||||
.SelectMany(a => a)
|
||||
.Where(a => a.IsMoveTarget)
|
||||
.Cast<IMailItemFolder>()
|
||||
.ToList();
|
||||
|
||||
return categoriesByAccount
|
||||
.SelectMany(a => a.Categories)
|
||||
.GroupBy(a => NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => (IMenuItem)new MergedMailCategoryMenuItem(group.ToList(), handlingFolders, mergedInbox))
|
||||
.OrderBy(item => ((MergedMailCategoryMenuItem)item).FolderName, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string NormalizeCategoryName(string name)
|
||||
=> name?.Trim() ?? string.Empty;
|
||||
|
||||
private HashSet<SpecialFolderType> FindCommonFolders(List<List<MailItemFolder>> lists)
|
||||
{
|
||||
var allSpecialTypesExceptOther = Enum.GetValues<SpecialFolderType>().Cast<SpecialFolderType>().Where(a => a != SpecialFolderType.Other).ToList();
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Messaging.Client.Accounts;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class MailCategoryService : BaseDatabaseService, IMailCategoryService
|
||||
{
|
||||
public MailCategoryService(IDatabaseService databaseService) : base(databaseService)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<List<MailCategory>> GetCategoriesAsync(Guid accountId)
|
||||
=> Connection.QueryAsync<MailCategory>(
|
||||
$"SELECT * FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? ORDER BY {nameof(MailCategory.IsFavorite)} DESC, {nameof(MailCategory.Name)} COLLATE NOCASE",
|
||||
accountId);
|
||||
|
||||
public Task<List<MailCategory>> GetFavoriteCategoriesAsync(Guid accountId)
|
||||
=> Connection.QueryAsync<MailCategory>(
|
||||
$"SELECT * FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? AND {nameof(MailCategory.IsFavorite)} = 1 ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE",
|
||||
accountId);
|
||||
|
||||
public Task<MailCategory> GetCategoryAsync(Guid categoryId)
|
||||
=> Connection.FindAsync<MailCategory>(categoryId);
|
||||
|
||||
public async Task<bool> CategoryNameExistsAsync(Guid accountId, string name, Guid? excludedCategoryId = null)
|
||||
{
|
||||
var normalizedName = NormalizeCategoryName(name);
|
||||
if (string.IsNullOrWhiteSpace(normalizedName))
|
||||
return false;
|
||||
|
||||
var sql = $"SELECT COUNT(*) FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? AND lower(trim({nameof(MailCategory.Name)})) = ?";
|
||||
var parameters = new List<object> { accountId, normalizedName.ToLowerInvariant() };
|
||||
|
||||
if (excludedCategoryId.HasValue)
|
||||
{
|
||||
sql += $" AND {nameof(MailCategory.Id)} <> ?";
|
||||
parameters.Add(excludedCategoryId.Value);
|
||||
}
|
||||
|
||||
return await Connection.ExecuteScalarAsync<int>(sql, parameters.ToArray()).ConfigureAwait(false) > 0;
|
||||
}
|
||||
|
||||
public async Task<MailCategory> CreateCategoryAsync(MailCategory category)
|
||||
{
|
||||
category.Id = category.Id == Guid.Empty ? Guid.NewGuid() : category.Id;
|
||||
category.Name = NormalizeCategoryName(category.Name);
|
||||
|
||||
await Connection.InsertAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
public async Task UpdateCategoryAsync(MailCategory category)
|
||||
{
|
||||
category.Name = NormalizeCategoryName(category.Name);
|
||||
|
||||
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
||||
}
|
||||
|
||||
public async Task DeleteCategoryAsync(Guid categoryId)
|
||||
{
|
||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
||||
if (category == null)
|
||||
return;
|
||||
|
||||
await Connection.ExecuteAsync($"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ?", categoryId).ConfigureAwait(false);
|
||||
await Connection.DeleteAsync<MailCategory>(categoryId).ConfigureAwait(false);
|
||||
|
||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
||||
}
|
||||
|
||||
public async Task DeleteCategoriesAsync(Guid accountId)
|
||||
{
|
||||
var categories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
if (categories.Count == 0)
|
||||
return;
|
||||
|
||||
var categoryIds = categories.Select(a => a.Id).ToList();
|
||||
var placeholders = string.Join(",", categoryIds.Select(_ => "?"));
|
||||
var deleteAssignmentsSql = $"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} IN ({placeholders})";
|
||||
|
||||
await Connection.ExecuteAsync(deleteAssignmentsSql, categoryIds.Cast<object>().ToArray()).ConfigureAwait(false);
|
||||
await Connection.Table<MailCategory>().DeleteAsync(a => a.MailAccountId == accountId).ConfigureAwait(false);
|
||||
|
||||
NotifyCategoryStructureChanged(accountId);
|
||||
}
|
||||
|
||||
public async Task ToggleFavoriteAsync(Guid categoryId, bool isFavorite)
|
||||
{
|
||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
||||
if (category == null || category.IsFavorite == isFavorite)
|
||||
return;
|
||||
|
||||
category.IsFavorite = isFavorite;
|
||||
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
||||
|
||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
||||
}
|
||||
|
||||
public async Task UpdateRemoteIdAsync(Guid categoryId, string remoteId)
|
||||
{
|
||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
||||
if (category == null)
|
||||
return;
|
||||
|
||||
category.RemoteId = remoteId;
|
||||
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task ReplaceCategoriesAsync(Guid accountId, IEnumerable<MailCategory> categories)
|
||||
{
|
||||
var existingCategories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
|
||||
var existingByRemoteId = existingCategories
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.RemoteId))
|
||||
.ToDictionary(a => a.RemoteId, StringComparer.OrdinalIgnoreCase);
|
||||
var existingByName = existingCategories
|
||||
.GroupBy(a => NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(a => a.Key, a => a.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var incomingCategories = categories?.ToList() ?? [];
|
||||
var preservedIds = new HashSet<Guid>();
|
||||
|
||||
foreach (var incoming in incomingCategories)
|
||||
{
|
||||
incoming.MailAccountId = accountId;
|
||||
incoming.Id = incoming.Id == Guid.Empty ? Guid.NewGuid() : incoming.Id;
|
||||
incoming.Name = NormalizeCategoryName(incoming.Name);
|
||||
|
||||
MailCategory existing = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(incoming.RemoteId) && existingByRemoteId.TryGetValue(incoming.RemoteId, out var byRemote))
|
||||
{
|
||||
existing = byRemote;
|
||||
}
|
||||
else if (existingByName.TryGetValue(incoming.Name, out var byName))
|
||||
{
|
||||
existing = byName;
|
||||
}
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
await Connection.InsertAsync(incoming, typeof(MailCategory)).ConfigureAwait(false);
|
||||
preservedIds.Add(incoming.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
incoming.Id = existing.Id;
|
||||
incoming.IsFavorite = existing.IsFavorite;
|
||||
await Connection.UpdateAsync(incoming, typeof(MailCategory)).ConfigureAwait(false);
|
||||
preservedIds.Add(existing.Id);
|
||||
}
|
||||
}
|
||||
|
||||
var categoryIdsToDelete = existingCategories
|
||||
.Where(a => !preservedIds.Contains(a.Id))
|
||||
.Select(a => a.Id)
|
||||
.ToList();
|
||||
|
||||
if (categoryIdsToDelete.Count > 0)
|
||||
{
|
||||
var placeholders = string.Join(",", categoryIdsToDelete.Select(_ => "?"));
|
||||
await Connection.ExecuteAsync(
|
||||
$"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} IN ({placeholders})",
|
||||
categoryIdsToDelete.Cast<object>().ToArray()).ConfigureAwait(false);
|
||||
|
||||
foreach (var categoryId in categoryIdsToDelete)
|
||||
{
|
||||
await Connection.DeleteAsync<MailCategory>(categoryId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
NotifyCategoryStructureChanged(accountId);
|
||||
}
|
||||
|
||||
public async Task ReplaceMailAssignmentsAsync(Guid accountId, Guid mailCopyUniqueId, IEnumerable<string> categoryNames)
|
||||
{
|
||||
var normalizedNames = categoryNames?
|
||||
.Select(NormalizeCategoryName)
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList() ?? [];
|
||||
|
||||
var availableCategories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
|
||||
var categoryIds = availableCategories
|
||||
.Where(a => normalizedNames.Contains(NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase))
|
||||
.Select(a => a.Id)
|
||||
.ToHashSet();
|
||||
|
||||
var existingAssignments = await Connection.QueryAsync<MailCategoryAssignment>(
|
||||
$"SELECT * FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} = ?",
|
||||
mailCopyUniqueId).ConfigureAwait(false);
|
||||
|
||||
var assignmentsToDelete = existingAssignments.Where(a => !categoryIds.Contains(a.MailCategoryId)).ToList();
|
||||
var existingIds = existingAssignments.Select(a => a.MailCategoryId).ToHashSet();
|
||||
var assignmentsToAdd = categoryIds.Where(a => !existingIds.Contains(a)).ToList();
|
||||
|
||||
foreach (var assignment in assignmentsToDelete)
|
||||
{
|
||||
await Connection.DeleteAsync<MailCategoryAssignment>(assignment.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var categoryId in assignmentsToAdd)
|
||||
{
|
||||
await Connection.InsertAsync(new MailCategoryAssignment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MailCategoryId = categoryId,
|
||||
MailCopyUniqueId = mailCopyUniqueId
|
||||
}, typeof(MailCategoryAssignment)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(accountId));
|
||||
}
|
||||
|
||||
public async Task AssignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds)
|
||||
{
|
||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
||||
if (uniqueIds.Count == 0)
|
||||
return;
|
||||
|
||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
||||
if (category == null)
|
||||
return;
|
||||
|
||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
||||
var query = $"SELECT * FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ? AND {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders})";
|
||||
var existingAssignments = await Connection.QueryAsync<MailCategoryAssignment>(
|
||||
query,
|
||||
[categoryId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
|
||||
var existingUniqueIds = existingAssignments.Select(a => a.MailCopyUniqueId).ToHashSet();
|
||||
|
||||
foreach (var uniqueId in uniqueIds.Where(a => !existingUniqueIds.Contains(a)))
|
||||
{
|
||||
await Connection.InsertAsync(new MailCategoryAssignment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MailCategoryId = categoryId,
|
||||
MailCopyUniqueId = uniqueId
|
||||
}, typeof(MailCategoryAssignment)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(category.MailAccountId));
|
||||
}
|
||||
|
||||
public async Task UnassignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds)
|
||||
{
|
||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
||||
if (uniqueIds.Count == 0)
|
||||
return;
|
||||
|
||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
||||
if (category == null)
|
||||
return;
|
||||
|
||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
||||
await Connection.ExecuteAsync(
|
||||
$"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ? AND {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders})",
|
||||
[categoryId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(category.MailAccountId));
|
||||
}
|
||||
|
||||
public async Task<List<MailCategory>> GetCategoriesForMailAsync(Guid accountId, IEnumerable<Guid> mailCopyUniqueIds)
|
||||
{
|
||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
||||
if (uniqueIds.Count == 0)
|
||||
return [];
|
||||
|
||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
||||
var sql = $"SELECT DISTINCT MailCategory.* FROM {nameof(MailCategory)} " +
|
||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCategoryId)} " +
|
||||
$"WHERE {nameof(MailCategory)}.{nameof(MailCategory.MailAccountId)} = ? AND {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders}) " +
|
||||
$"ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE";
|
||||
|
||||
return await Connection.QueryAsync<MailCategory>(
|
||||
sql,
|
||||
[accountId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> GetAssignedCategoryIdsForAllAsync(IEnumerable<Guid> mailCopyUniqueIds)
|
||||
{
|
||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
||||
if (uniqueIds.Count == 0)
|
||||
return [];
|
||||
|
||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
||||
var sql = $"SELECT {nameof(MailCategoryAssignment.MailCategoryId)} " +
|
||||
$"FROM {nameof(MailCategoryAssignment)} " +
|
||||
$"WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders}) " +
|
||||
$"GROUP BY {nameof(MailCategoryAssignment.MailCategoryId)} " +
|
||||
$"HAVING COUNT(DISTINCT {nameof(MailCategoryAssignment.MailCopyUniqueId)}) = ?";
|
||||
|
||||
return await Connection.QueryScalarsAsync<Guid>(
|
||||
sql,
|
||||
[.. uniqueIds.Cast<object>(), uniqueIds.Count]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetCategoryNamesForMailAsync(Guid mailCopyUniqueId)
|
||||
{
|
||||
var sql = $"SELECT {nameof(MailCategory.Name)} " +
|
||||
$"FROM {nameof(MailCategory)} " +
|
||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment.MailCategoryId)} " +
|
||||
$"WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} = ? " +
|
||||
$"ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE";
|
||||
|
||||
return await Connection.QueryScalarsAsync<string>(sql, mailCopyUniqueId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<MailCopy>> GetMailCopiesForCategoryAsync(Guid categoryId)
|
||||
{
|
||||
var sql = $"SELECT {nameof(MailCopy)}.* " +
|
||||
$"FROM {nameof(MailCopy)} " +
|
||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCopy)}.{nameof(MailCopy.UniqueId)} = {nameof(MailCategoryAssignment.MailCopyUniqueId)} " +
|
||||
$"WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ?";
|
||||
|
||||
return await Connection.QueryAsync<MailCopy>(sql, categoryId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<UnreadCategoryCountResult>> GetUnreadCategoryCountResultsAsync(IEnumerable<Guid> accountIds)
|
||||
{
|
||||
var accountIdList = accountIds?.Distinct().ToList() ?? [];
|
||||
if (accountIdList.Count == 0)
|
||||
return Task.FromResult(new List<UnreadCategoryCountResult>());
|
||||
|
||||
var placeholders = string.Join(",", accountIdList.Select(_ => "?"));
|
||||
var sql =
|
||||
$"SELECT MailCategory.{nameof(MailCategory.Id)} as {nameof(UnreadCategoryCountResult.CategoryId)}, " +
|
||||
$"MailCategory.{nameof(MailCategory.MailAccountId)} as {nameof(UnreadCategoryCountResult.AccountId)}, " +
|
||||
$"COUNT(DISTINCT MailCopy.{nameof(MailCopy.UniqueId)}) as {nameof(UnreadCategoryCountResult.UnreadItemCount)} " +
|
||||
$"FROM {nameof(MailCategory)} " +
|
||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCategoryId)} " +
|
||||
$"INNER JOIN {nameof(MailCopy)} ON {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCopyUniqueId)} = {nameof(MailCopy)}.{nameof(MailCopy.UniqueId)} " +
|
||||
$"WHERE MailCategory.{nameof(MailCategory.MailAccountId)} IN ({placeholders}) AND MailCopy.{nameof(MailCopy.IsRead)} = 0 " +
|
||||
$"GROUP BY MailCategory.{nameof(MailCategory.Id)}";
|
||||
|
||||
return Connection.QueryAsync<UnreadCategoryCountResult>(sql, accountIdList.Cast<object>().ToArray());
|
||||
}
|
||||
|
||||
private void NotifyCategoryStructureChanged(Guid accountId)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested(false));
|
||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(accountId));
|
||||
}
|
||||
|
||||
private static string NormalizeCategoryName(string name)
|
||||
=> name?.Trim() ?? string.Empty;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly ISentMailReceiptService _sentMailReceiptService;
|
||||
private readonly IMailCategoryService _mailCategoryService;
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<MailService>();
|
||||
|
||||
@@ -42,7 +43,8 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
ISignatureService signatureService,
|
||||
IMimeFileService mimeFileService,
|
||||
IPreferencesService preferencesService,
|
||||
ISentMailReceiptService sentMailReceiptService) : base(databaseService)
|
||||
ISentMailReceiptService sentMailReceiptService,
|
||||
IMailCategoryService mailCategoryService) : base(databaseService)
|
||||
{
|
||||
_folderService = folderService;
|
||||
_contactService = contactService;
|
||||
@@ -51,6 +53,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
_mimeFileService = mimeFileService;
|
||||
_preferencesService = preferencesService;
|
||||
_sentMailReceiptService = sentMailReceiptService;
|
||||
_mailCategoryService = mailCategoryService;
|
||||
}
|
||||
|
||||
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
||||
@@ -171,7 +174,9 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
private static (string Query, object[] Parameters) BuildMailFetchQuery(MailListInitializationOptions options)
|
||||
{
|
||||
var sql = new StringBuilder();
|
||||
sql.Append("SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id");
|
||||
sql.Append(options.IsCategoryView
|
||||
? "SELECT DISTINCT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id INNER JOIN MailCategoryAssignment ON MailCopy.UniqueId = MailCategoryAssignment.MailCopyUniqueId"
|
||||
: "SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id");
|
||||
|
||||
var whereClauses = new List<string>();
|
||||
var parameters = new List<object>();
|
||||
@@ -181,6 +186,13 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
whereClauses.Add($"MailCopy.FolderId IN ({folderPlaceholders})");
|
||||
parameters.AddRange(options.Folders.Select(f => (object)f.Id));
|
||||
|
||||
if (options.IsCategoryView)
|
||||
{
|
||||
var categoryPlaceholders = string.Join(",", options.CategoryIds.Select(_ => "?"));
|
||||
whereClauses.Add($"MailCategoryAssignment.MailCategoryId IN ({categoryPlaceholders})");
|
||||
parameters.AddRange(options.CategoryIds.Select(a => (object)a));
|
||||
}
|
||||
|
||||
// Filter type
|
||||
switch (options.FilterType)
|
||||
{
|
||||
@@ -338,7 +350,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
{
|
||||
List<MailCopy> mails;
|
||||
|
||||
if (options.PreFetchMailCopies != null)
|
||||
if (options.PreFetchMailCopies != null && !options.IsCategoryView)
|
||||
{
|
||||
mails = ApplyOptionsToPreFetchedMails(options);
|
||||
}
|
||||
@@ -398,7 +410,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
mails.RemoveAll(m => m.AssignedAccount == null || m.AssignedFolder == null);
|
||||
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
|
||||
|
||||
if (!options.CreateThreads || mails.Count == 0)
|
||||
if (!options.CreateThreads || mails.Count == 0 || options.IsCategoryView)
|
||||
return [.. mails];
|
||||
|
||||
// 6. Expand threads: one batch query for all sibling mails across all threads.
|
||||
@@ -727,6 +739,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
_logger.Debug("Deleting mail {Id} from folder {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName);
|
||||
|
||||
await Connection.DeleteAsync<MailCopy>(mailCopy.UniqueId).ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("DELETE FROM MailCategoryAssignment WHERE MailCopyUniqueId = ?", mailCopy.UniqueId).ConfigureAwait(false);
|
||||
|
||||
// If there are no more copies exists of the same mail, delete the MIME file as well.
|
||||
var isMailExists = await IsMailExistsAsync(mailCopy.Id).ConfigureAwait(false);
|
||||
@@ -965,6 +978,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
mailCopy.UniqueId = existingCopyItem.UniqueId;
|
||||
|
||||
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
|
||||
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
|
||||
@@ -981,6 +995,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
}
|
||||
|
||||
await InsertMailAsync(mailCopy).ConfigureAwait(false);
|
||||
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
|
||||
@@ -1017,6 +1032,11 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task ReplaceMailCategoriesForPackageAsync(Guid accountId, MailCopy mailCopy, NewMailItemPackage package)
|
||||
=> package?.CategoryNames == null
|
||||
? Task.CompletedTask
|
||||
: _mailCategoryService.ReplaceMailAssignmentsAsync(accountId, mailCopy.UniqueId, package.CategoryNames);
|
||||
|
||||
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions, MailAccountAlias selectedAlias)
|
||||
{
|
||||
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
|
||||
|
||||
@@ -20,6 +20,7 @@ public static class ServicesContainerSetup
|
||||
|
||||
services.AddTransient<ICalendarService, CalendarService>();
|
||||
services.AddTransient<IMailService, MailService>();
|
||||
services.AddTransient<IMailCategoryService, MailCategoryService>();
|
||||
services.AddTransient<ISentMailReceiptService, SentMailReceiptService>();
|
||||
services.AddTransient<IFolderService, FolderService>();
|
||||
services.AddTransient<IAccountService, AccountService>();
|
||||
|
||||
Reference in New Issue
Block a user