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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user