Contacts, thread animation and image preview control improvements.

This commit is contained in:
Burak Kaan Köse
2026-02-09 22:39:30 +01:00
parent e559a79506
commit 0999c71578
26 changed files with 1636 additions and 756 deletions
+89 -3
View File
@@ -289,6 +289,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
var folders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
var syncableFolders = folders
.Where(f => f.IsSynchronizationEnabled && f.RemoteFolderId != ServiceConstants.ARCHIVE_LABEL_ID)
.OrderByDescending(f => f.SpecialFolderType == SpecialFolderType.Draft || f.RemoteFolderId == ServiceConstants.DRAFT_LABEL_ID)
.ToList();
var totalFolders = syncableFolders.Count;
@@ -328,8 +329,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
if (newMessageIds.Count > 0)
{
// Download metadata in batches (no raw MIME during initial sync)
await DownloadMessagesInBatchAsync(newMessageIds, downloadRawMime: false, cancellationToken).ConfigureAwait(false);
// Draft folder needs MIME during initial sync so compose can open immediately.
bool shouldDownloadRawMime = folder.SpecialFolderType == SpecialFolderType.Draft || folder.RemoteFolderId == ServiceConstants.DRAFT_LABEL_ID;
await DownloadMessagesInBatchAsync(newMessageIds, downloadRawMime: shouldDownloadRawMime, cancellationToken).ConfigureAwait(false);
foreach (var id in newMessageIds)
{
@@ -1848,6 +1850,88 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
return ("", emailOnly);
}
private static IReadOnlyList<AccountContact> ExtractContactsFromGmailMessage(Message message, MimeMessage mimeMessage)
{
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
AddFromHeaders(message?.Payload?.Headers);
if (mimeMessage != null)
{
AddFromInternetAddressList(mimeMessage.From);
AddFromInternetAddressList(mimeMessage.To);
AddFromInternetAddressList(mimeMessage.Cc);
AddFromInternetAddressList(mimeMessage.Bcc);
AddFromInternetAddressList(mimeMessage.ReplyTo);
if (mimeMessage.Sender is MailboxAddress senderMailbox)
{
AddContact(senderMailbox.Address, senderMailbox.Name);
}
}
return contacts.Values.ToList();
void AddFromHeaders(IList<MessagePartHeader> headers)
{
if (headers == null || headers.Count == 0) return;
AddFromHeader("From");
AddFromHeader("Sender");
AddFromHeader("To");
AddFromHeader("Cc");
AddFromHeader("Bcc");
AddFromHeader("Reply-To");
void AddFromHeader(string headerName)
{
var headerValue = headers
.FirstOrDefault(h => h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase))
?.Value;
if (string.IsNullOrWhiteSpace(headerValue)) return;
try
{
var addresses = InternetAddressList.Parse(headerValue);
foreach (var mailbox in addresses.Mailboxes)
{
AddContact(mailbox.Address, mailbox.Name);
}
}
catch
{
var (name, email) = ExtractNameAndEmailFromHeader(headerValue);
AddContact(email, name);
}
}
}
void AddFromInternetAddressList(InternetAddressList addresses)
{
if (addresses == null) return;
foreach (var mailbox in addresses.Mailboxes)
{
AddContact(mailbox.Address, mailbox.Name);
}
}
void AddContact(string address, string name)
{
var trimmedAddress = address?.Trim();
if (string.IsNullOrWhiteSpace(trimmedAddress)) return;
var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim();
contacts[trimmedAddress] = new AccountContact
{
Address = trimmedAddress,
Name = displayName
};
}
}
/// <summary>
/// Creates new mail packages for the given message.
/// AssignedFolder is null since the LabelId is parsed out of the Message.
@@ -1887,6 +1971,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
EnrichMailCopyFromMime(baseMailCopy, mimeMessage);
}
var extractedContacts = ExtractContactsFromGmailMessage(message, mimeMessage);
// Check for local draft mapping using X-Wino-Draft-Id header.
// For Metadata format we read from Payload.Headers.
// For Raw format (Payload is null), we read from parsed MIME headers.
@@ -1962,7 +2048,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
mailCopyForLabel.Id = sharedId;
mailCopyForLabel.FileId = sharedFileId;
packageList.Add(new NewMailItemPackage(mailCopyForLabel, mimeMessage, labelId));
packageList.Add(new NewMailItemPackage(mailCopyForLabel, mimeMessage, labelId, extractedContacts));
}
}
+47 -1
View File
@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MimeKit;
using MoreLinq;
using Serilog;
using Wino.Core.Domain.Entities.Mail;
@@ -334,7 +335,8 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
// Local copy doesn't exists. Continue execution to insert mail copy.
}
var package = new NewMailItemPackage(mailCopy, message.MimeMessage, assignedFolder.RemoteFolderId);
var contacts = ExtractContactsFromMimeMessage(message.MimeMessage);
var package = new NewMailItemPackage(mailCopy, message.MimeMessage, assignedFolder.RemoteFolderId, contacts);
return
[
@@ -342,6 +344,50 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
];
}
private static IReadOnlyList<AccountContact> ExtractContactsFromMimeMessage(MimeMessage mimeMessage)
{
if (mimeMessage == null) return [];
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
AddFromInternetAddressList(mimeMessage.From);
AddFromInternetAddressList(mimeMessage.To);
AddFromInternetAddressList(mimeMessage.Cc);
AddFromInternetAddressList(mimeMessage.Bcc);
AddFromInternetAddressList(mimeMessage.ReplyTo);
if (mimeMessage.Sender is MailboxAddress senderMailbox)
{
AddContact(senderMailbox.Address, senderMailbox.Name);
}
return contacts.Values.ToList();
void AddFromInternetAddressList(InternetAddressList addresses)
{
if (addresses == null) return;
foreach (var mailbox in addresses.Mailboxes)
{
AddContact(mailbox.Address, mailbox.Name);
}
}
void AddContact(string address, string name)
{
var trimmedAddress = address?.Trim();
if (string.IsNullOrWhiteSpace(trimmedAddress)) return;
var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim();
contacts[trimmedAddress] = new AccountContact
{
Address = trimmedAddress,
Name = displayName
};
}
}
protected override async Task<MailSynchronizationResult> SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();
+104 -17
View File
@@ -90,6 +90,11 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
"Id",
"ConversationId",
"From",
"Sender",
"ToRecipients",
"CcRecipients",
"BccRecipients",
"ReplyTo",
"Subject",
"ParentFolderId",
"InternetMessageId",
@@ -375,28 +380,50 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
if (!mailExists)
{
// Create MailCopy from metadata
var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false);
if (mailCopy != null)
// For drafts, download MIME during initial sync like delta sync.
if (folder.SpecialFolderType == SpecialFolderType.Draft)
{
// Create package without MIME
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
var draftPackages = await CreateNewMailPackagesAsync(message, folder, cancellationToken).ConfigureAwait(false);
if (isInserted)
if (draftPackages != null)
{
downloadedMessageIds.Add(mailCopy.Id);
totalProcessed++;
// Update progress periodically
if (totalProcessed % 50 == 0)
foreach (var package in draftPackages)
{
var statusMessage = string.Format(Translator.Sync_DownloadedMessages, totalProcessed, folder.FolderName);
UpdateSyncProgress(0, 0, statusMessage);
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
if (isInserted)
{
downloadedMessageIds.Add(package.Copy.Id);
totalProcessed++;
}
}
}
}
else
{
// Create MailCopy from metadata
var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false);
if (mailCopy != null)
{
// Create package without MIME
var contacts = ExtractContactsFromOutlookMessage(message);
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts);
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
if (isInserted)
{
downloadedMessageIds.Add(mailCopy.Id);
totalProcessed++;
}
}
}
// Update progress periodically
if (totalProcessed > 0 && totalProcessed % 50 == 0)
{
var statusMessage = string.Format(Translator.Sync_DownloadedMessages, totalProcessed, folder.FolderName);
UpdateSyncProgress(0, 0, statusMessage);
}
}
else
{
@@ -551,7 +578,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
if (mailCopy != null)
{
// Create package without MIME
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
var contacts = ExtractContactsFromOutlookMessage(message);
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts);
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
if (isInserted)
@@ -686,6 +714,64 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
return mailCopy;
}
private static IReadOnlyList<AccountContact> ExtractContactsFromOutlookMessage(Message message)
{
if (message == null) return [];
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
AddRecipient(message.From?.EmailAddress);
AddRecipient(message.Sender?.EmailAddress);
if (message.ToRecipients != null)
{
foreach (var recipient in message.ToRecipients)
{
AddRecipient(recipient?.EmailAddress);
}
}
if (message.CcRecipients != null)
{
foreach (var recipient in message.CcRecipients)
{
AddRecipient(recipient?.EmailAddress);
}
}
if (message.BccRecipients != null)
{
foreach (var recipient in message.BccRecipients)
{
AddRecipient(recipient?.EmailAddress);
}
}
if (message.ReplyTo != null)
{
foreach (var recipient in message.ReplyTo)
{
AddRecipient(recipient?.EmailAddress);
}
}
return contacts.Values.ToList();
void AddRecipient(EmailAddress emailAddress)
{
var address = emailAddress?.Address?.Trim();
if (string.IsNullOrWhiteSpace(address)) return;
var displayName = string.IsNullOrWhiteSpace(emailAddress.Name) ? address : emailAddress.Name.Trim();
contacts[address] = new AccountContact
{
Address = address,
Name = displayName
};
}
}
private string GetDeltaTokenFromDeltaLink(string deltaLink)
=> Regex.Split(deltaLink, "deltatoken=")[1];
@@ -1790,7 +1876,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
// Outlook messages can only be assigned to 1 folder at a time.
// Therefore we don't need to create multiple copies of the same message for different folders.
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId);
var contacts = ExtractContactsFromOutlookMessage(message);
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts);
return [package];
}