Contacts, thread animation and image preview control improvements.
This commit is contained in:
+125
-20
@@ -6,6 +6,7 @@ using MimeKit;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Contacts;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services;
|
||||
@@ -38,32 +39,91 @@ public class ContactService : BaseDatabaseService, IContactService
|
||||
|
||||
public async Task SaveAddressInformationAsync(MimeMessage message)
|
||||
{
|
||||
var recipients = message
|
||||
.GetRecipients(true)
|
||||
.Where(a => !string.IsNullOrEmpty(a.Name) && !string.IsNullOrEmpty(a.Address));
|
||||
if (message == null) return;
|
||||
|
||||
var addressInformations = recipients.Select(a => new AccountContact() { Name = a.Name, Address = a.Address });
|
||||
var contacts = message
|
||||
.GetRecipients(true)
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.Address))
|
||||
.Select(a => new AccountContact
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(a.Name) ? a.Address : a.Name,
|
||||
Address = a.Address
|
||||
});
|
||||
|
||||
foreach (var info in addressInformations)
|
||||
await SaveAddressInformationInternalAsync(contacts).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SaveAddressInformationAsync(IEnumerable<AccountContact> contacts)
|
||||
{
|
||||
if (contacts == null) return;
|
||||
|
||||
await SaveAddressInformationInternalAsync(contacts).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SaveAddressInformationInternalAsync(IEnumerable<AccountContact> contacts)
|
||||
{
|
||||
var addressInformations = contacts
|
||||
.Where(a => a != null && !string.IsNullOrWhiteSpace(a.Address))
|
||||
.Select(a => new AccountContact
|
||||
{
|
||||
Address = a.Address.Trim(),
|
||||
Name = string.IsNullOrWhiteSpace(a.Name) ? a.Address.Trim() : a.Name.Trim()
|
||||
})
|
||||
.GroupBy(a => a.Address, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
if (addressInformations.Count == 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
var currentContact = await GetAddressInformationByAddressAsync(info.Address).ConfigureAwait(false);
|
||||
// Batch-fetch all existing contacts in one query.
|
||||
var addresses = addressInformations.Select(a => a.Address).ToList();
|
||||
var placeholders = string.Join(",", addresses.Select((_, i) => "?"));
|
||||
var existingContacts = await Connection.QueryAsync<AccountContact>(
|
||||
$"SELECT * FROM AccountContact WHERE Address IN ({placeholders})",
|
||||
addresses.Cast<object>().ToArray()
|
||||
).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
var existingLookup = existingContacts.ToDictionary(c => c.Address, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var toInsert = new List<AccountContact>();
|
||||
var toUpdate = new List<AccountContact>();
|
||||
|
||||
foreach (var info in addressInformations)
|
||||
{
|
||||
if (currentContact == null)
|
||||
if (!existingLookup.TryGetValue(info.Address, out var existing))
|
||||
{
|
||||
await Connection.InsertAsync(info, typeof(AccountContact)).ConfigureAwait(false);
|
||||
toInsert.Add(info);
|
||||
}
|
||||
else if (!currentContact.IsRootContact && !currentContact.IsOverridden) // Don't update root contacts or overridden contacts.
|
||||
else if (!existing.IsRootContact && !existing.IsOverridden)
|
||||
{
|
||||
await Connection.InsertOrReplaceAsync(info, typeof(AccountContact)).ConfigureAwait(false);
|
||||
// Only update if the new name is more informative (not just the email address)
|
||||
// and actually different from the current name.
|
||||
if (info.Name != info.Address && existing.Name != info.Name)
|
||||
{
|
||||
existing.Name = info.Name;
|
||||
toUpdate.Add(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (toInsert.Count > 0 || toUpdate.Count > 0)
|
||||
{
|
||||
Log.Error("Failed to add contact information to the database.", ex);
|
||||
await Connection.RunInTransactionAsync(conn =>
|
||||
{
|
||||
if (toInsert.Count > 0)
|
||||
conn.InsertAll(toInsert, typeof(AccountContact));
|
||||
|
||||
foreach (var contact in toUpdate)
|
||||
conn.Update(contact, typeof(AccountContact));
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to batch save contact information to the database.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<AccountContact>> GetAllContactsAsync()
|
||||
@@ -81,20 +141,61 @@ public class ContactService : BaseDatabaseService, IContactService
|
||||
return Connection.QueryAsync<AccountContact>(query, pattern, pattern);
|
||||
}
|
||||
|
||||
public async Task<PagedContactsResult> GetContactsPageAsync(int offset, int pageSize, string searchQuery = null, bool excludeRootContacts = false)
|
||||
{
|
||||
offset = Math.Max(0, offset);
|
||||
pageSize = Math.Max(1, pageSize);
|
||||
|
||||
var whereClauses = new List<string>();
|
||||
var parameters = new List<object>();
|
||||
|
||||
if (excludeRootContacts)
|
||||
{
|
||||
whereClauses.Add("IsRootContact = 0");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchQuery))
|
||||
{
|
||||
var pattern = $"%{searchQuery.Trim()}%";
|
||||
whereClauses.Add("(Address LIKE ? OR Name LIKE ?)");
|
||||
parameters.Add(pattern);
|
||||
parameters.Add(pattern);
|
||||
}
|
||||
|
||||
var whereSql = whereClauses.Count > 0
|
||||
? $" WHERE {string.Join(" AND ", whereClauses)}"
|
||||
: string.Empty;
|
||||
|
||||
var countQuery = $"SELECT COUNT(*) FROM AccountContact{whereSql}";
|
||||
var totalCount = await Connection.ExecuteScalarAsync<int>(countQuery, parameters.ToArray()).ConfigureAwait(false);
|
||||
|
||||
var pageParameters = new List<object>(parameters)
|
||||
{
|
||||
pageSize,
|
||||
offset
|
||||
};
|
||||
|
||||
var pageQuery = $"SELECT * FROM AccountContact{whereSql} ORDER BY COALESCE(Name, Address) COLLATE NOCASE, Address COLLATE NOCASE LIMIT ? OFFSET ?";
|
||||
var contacts = await Connection.QueryAsync<AccountContact>(pageQuery, pageParameters.ToArray()).ConfigureAwait(false);
|
||||
var hasMore = offset + contacts.Count < totalCount;
|
||||
|
||||
return new PagedContactsResult(contacts, totalCount, hasMore, offset, pageSize);
|
||||
}
|
||||
|
||||
public async Task<AccountContact> UpdateContactAsync(AccountContact contact)
|
||||
{
|
||||
// Mark the contact as overridden when manually updated
|
||||
contact.IsOverridden = true;
|
||||
|
||||
|
||||
await Connection.UpdateAsync(contact, typeof(AccountContact)).ConfigureAwait(false);
|
||||
|
||||
|
||||
return contact;
|
||||
}
|
||||
|
||||
public async Task DeleteContactAsync(string address)
|
||||
{
|
||||
var contact = await GetAddressInformationByAddressAsync(address).ConfigureAwait(false);
|
||||
|
||||
|
||||
if (contact != null && !contact.IsRootContact)
|
||||
{
|
||||
await Connection.DeleteAsync<AccountContact>(contact.Address).ConfigureAwait(false);
|
||||
@@ -103,9 +204,13 @@ public class ContactService : BaseDatabaseService, IContactService
|
||||
|
||||
public async Task DeleteContactsAsync(IEnumerable<string> addresses)
|
||||
{
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
await DeleteContactAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
var addressList = addresses.Where(a => !string.IsNullOrEmpty(a)).ToList();
|
||||
if (addressList.Count == 0) return;
|
||||
|
||||
var placeholders = string.Join(",", addressList.Select((_, i) => "?"));
|
||||
await Connection.ExecuteAsync(
|
||||
$"DELETE FROM AccountContact WHERE Address IN ({placeholders}) AND IsRootContact = 0",
|
||||
addressList.Cast<object>().ToArray()
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,11 +715,12 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
mailCopy.SenderContact = await GetSenderContactForAccountAsync(account, mailCopy.FromAddress).ConfigureAwait(false);
|
||||
mailCopy.FolderId = mailItemFolder.Id;
|
||||
|
||||
await SaveContactsForPackageAsync(package).ConfigureAwait(false);
|
||||
|
||||
var mimeSaveTask = _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, account.Id);
|
||||
var contactSaveTask = _contactService.SaveAddressInformationAsync(mimeMessage);
|
||||
var insertMailTask = InsertMailAsync(mailCopy);
|
||||
|
||||
await Task.WhenAll(mimeSaveTask, contactSaveTask, insertMailTask).ConfigureAwait(false);
|
||||
await Task.WhenAll(mimeSaveTask, insertMailTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CreateMailAsyncEx(Guid accountId, NewMailItemPackage package)
|
||||
@@ -780,10 +781,11 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
}
|
||||
}
|
||||
|
||||
// Save contact information.
|
||||
await _contactService.SaveAddressInformationAsync(mimeMessage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Save contact information extracted from provider API or MIME before insert/update.
|
||||
await SaveContactsForPackageAsync(package).ConfigureAwait(false);
|
||||
|
||||
// Create mail copy in the database.
|
||||
// Update if exists.
|
||||
|
||||
@@ -814,6 +816,35 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveContactsForPackageAsync(NewMailItemPackage package)
|
||||
{
|
||||
if (package == null) return;
|
||||
|
||||
if (package.Mime != null)
|
||||
{
|
||||
await _contactService.SaveAddressInformationAsync(package.Mime).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var contacts = package.ExtractedContacts?
|
||||
.Where(c => c != null && !string.IsNullOrWhiteSpace(c.Address))
|
||||
.ToList() ?? new List<AccountContact>();
|
||||
|
||||
var senderAddress = package.Copy?.FromAddress;
|
||||
if (!string.IsNullOrWhiteSpace(senderAddress))
|
||||
{
|
||||
contacts.Add(new AccountContact
|
||||
{
|
||||
Address = senderAddress,
|
||||
Name = string.IsNullOrWhiteSpace(package.Copy?.FromName) ? senderAddress : package.Copy.FromName
|
||||
});
|
||||
}
|
||||
|
||||
if (contacts.Count == 0) return;
|
||||
|
||||
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions)
|
||||
{
|
||||
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
|
||||
|
||||
Reference in New Issue
Block a user