Search improvements.
This commit is contained in:
@@ -248,6 +248,20 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
// Gmail's Messages API doesn't expose Draft IDs, so we query the Drafts API separately.
|
// Gmail's Messages API doesn't expose Draft IDs, so we query the Drafts API separately.
|
||||||
// This ensures DraftId is correctly set for both Wino-created and externally-created drafts.
|
// This ensures DraftId is correctly set for both Wino-created and externally-created drafts.
|
||||||
await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false);
|
await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Keep virtual Archive folder assignments in sync with Gmail "in:archive" query.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MapArchivedMailsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warning(ex, "Failed to map Gmail archive folder for {Name}", Account.Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -1175,52 +1189,89 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
public override async Task<List<MailCopy>> OnlineSearchAsync(string queryText, List<IMailItemFolder> folders, CancellationToken cancellationToken = default)
|
public override async Task<List<MailCopy>> OnlineSearchAsync(string queryText, List<IMailItemFolder> folders, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var request = _gmailService.Users.Messages.List("me");
|
if (string.IsNullOrWhiteSpace(queryText))
|
||||||
request.Q = queryText;
|
return [];
|
||||||
request.MaxResults = 500; // Max 500 is returned.
|
|
||||||
|
|
||||||
string pageToken = null;
|
static bool IsArchiveFolder(IMailItemFolder folder)
|
||||||
|
=> folder?.SpecialFolderType == SpecialFolderType.Archive || folder?.RemoteFolderId == ServiceConstants.ARCHIVE_LABEL_ID;
|
||||||
|
|
||||||
List<Message> messagesToDownload = [];
|
var messageIds = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
do
|
async Task CollectMessageIdsAsync(UsersResource.MessagesResource.ListRequest request)
|
||||||
{
|
{
|
||||||
if (queryText.StartsWith("label:") || queryText.StartsWith("in:"))
|
string pageToken = null;
|
||||||
|
|
||||||
|
do
|
||||||
{
|
{
|
||||||
// Ignore the folders if the query starts with these keywords.
|
if (!string.IsNullOrEmpty(pageToken))
|
||||||
// User is trying to list everything.
|
{
|
||||||
}
|
request.PageToken = pageToken;
|
||||||
else if (folders?.Count > 0)
|
}
|
||||||
|
|
||||||
|
var response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (response.Messages == null || response.Messages.Count == 0) break;
|
||||||
|
|
||||||
|
foreach (var message in response.Messages)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(message.Id))
|
||||||
|
{
|
||||||
|
messageIds.Add(message.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken = response.NextPageToken;
|
||||||
|
} while (!string.IsNullOrEmpty(pageToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasScopedQuery = queryText.StartsWith("label:", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
queryText.StartsWith("in:", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasScopedQuery || folders?.Count == 0)
|
||||||
|
{
|
||||||
|
var request = _gmailService.Users.Messages.List("me");
|
||||||
|
request.Q = queryText;
|
||||||
|
request.MaxResults = 500;
|
||||||
|
|
||||||
|
await CollectMessageIdsAsync(request).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var folder in folders)
|
||||||
{
|
{
|
||||||
request.LabelIds = folders.Select(a => a.RemoteFolderId).ToList();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var request = _gmailService.Users.Messages.List("me");
|
||||||
|
request.MaxResults = 500;
|
||||||
|
|
||||||
|
if (IsArchiveFolder(folder))
|
||||||
|
{
|
||||||
|
// Gmail archive is virtual. Query via search operator instead of label id.
|
||||||
|
request.Q = $"in:archive {queryText}".Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
request.Q = queryText;
|
||||||
|
request.LabelIds = new List<string> { folder.RemoteFolderId };
|
||||||
|
}
|
||||||
|
|
||||||
|
await CollectMessageIdsAsync(request).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(pageToken))
|
if (messageIds.Count == 0)
|
||||||
{
|
return [];
|
||||||
request.PageToken = pageToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await request.ExecuteAsync(cancellationToken);
|
var messageIdList = messageIds.ToList();
|
||||||
if (response.Messages == null) break;
|
|
||||||
|
|
||||||
// Handle skipping manually
|
// Do not download messages that already exist locally.
|
||||||
messagesToDownload.AddRange(response.Messages);
|
var existingMessageIds = await _gmailChangeProcessor.AreMailsExistsAsync(messageIdList).ConfigureAwait(false);
|
||||||
|
var messagesToDownload = messageIdList.Except(existingMessageIds, StringComparer.Ordinal);
|
||||||
|
|
||||||
pageToken = response.NextPageToken;
|
// Download missing messages in batch with metadata only.
|
||||||
} while (!string.IsNullOrEmpty(pageToken));
|
await DownloadMessagesInBatchAsync(messagesToDownload, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Do not download messages that exists, but return them for listing.
|
|
||||||
|
|
||||||
var messageIds = messagesToDownload.Select(a => a.Id);
|
|
||||||
|
|
||||||
var downloadRequireMessageIds = messageIds.Except(await _gmailChangeProcessor.AreMailsExistsAsync(messageIds));
|
|
||||||
|
|
||||||
// Download missing messages in batch.
|
|
||||||
await DownloadMessagesInBatchAsync(downloadRequireMessageIds, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Get results from database and return.
|
// Get results from database and return.
|
||||||
|
return await _gmailChangeProcessor.GetMailCopiesAsync(messageIdList).ConfigureAwait(false);
|
||||||
return await _gmailChangeProcessor.GetMailCopiesAsync(messageIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1644,41 +1695,62 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
private async Task MapArchivedMailsAsync(CancellationToken cancellationToken)
|
private async Task MapArchivedMailsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (!archiveFolderId.HasValue) return;
|
||||||
|
|
||||||
var request = _gmailService.Users.Messages.List("me");
|
var request = _gmailService.Users.Messages.List("me");
|
||||||
request.Q = "in:archive";
|
request.Q = "in:archive";
|
||||||
request.MaxResults = InitialMessageDownloadCountPerFolder;
|
request.MaxResults = 500;
|
||||||
|
|
||||||
string pageToken = null;
|
string pageToken = null;
|
||||||
|
|
||||||
var archivedMessageIds = new List<string>();
|
var archivedMessageIds = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(pageToken)) request.PageToken = pageToken;
|
if (!string.IsNullOrEmpty(pageToken)) request.PageToken = pageToken;
|
||||||
|
|
||||||
var response = await request.ExecuteAsync(cancellationToken);
|
var response = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (response.Messages == null) break;
|
if (response.Messages == null) break;
|
||||||
|
|
||||||
foreach (var message in response.Messages)
|
foreach (var message in response.Messages)
|
||||||
{
|
{
|
||||||
if (archivedMessageIds.Contains(message.Id)) continue;
|
if (!string.IsNullOrEmpty(message.Id))
|
||||||
|
{
|
||||||
archivedMessageIds.Add(message.Id);
|
archivedMessageIds.Add(message.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageToken = response.NextPageToken;
|
pageToken = response.NextPageToken;
|
||||||
} while (!string.IsNullOrEmpty(pageToken));
|
} while (!string.IsNullOrEmpty(pageToken));
|
||||||
|
|
||||||
var result = await _gmailChangeProcessor.GetGmailArchiveComparisonResultAsync(archiveFolderId.Value, archivedMessageIds).ConfigureAwait(false);
|
var result = await _gmailChangeProcessor.GetGmailArchiveComparisonResultAsync(archiveFolderId.Value, archivedMessageIds.ToList()).ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var archiveAddedItem in result.Added)
|
var addedArchiveIds = result.Added.Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
var removedArchiveIds = result.Removed.Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
|
if (addedArchiveIds.Count > 0)
|
||||||
{
|
{
|
||||||
await HandleArchiveAssignmentAsync(archiveAddedItem);
|
// Archive sync can surface messages that were never downloaded before.
|
||||||
|
// Download metadata first so assignment creation can succeed.
|
||||||
|
var existingBeforeDownload = await _gmailChangeProcessor.AreMailsExistsAsync(addedArchiveIds).ConfigureAwait(false);
|
||||||
|
var missingArchiveIds = addedArchiveIds.Except(existingBeforeDownload, StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
|
if (missingArchiveIds.Count > 0)
|
||||||
|
{
|
||||||
|
await DownloadMessagesInBatchAsync(missingArchiveIds, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingAfterDownload = await _gmailChangeProcessor.AreMailsExistsAsync(addedArchiveIds).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var archiveAddedItem in existingAfterDownload)
|
||||||
|
{
|
||||||
|
await HandleArchiveAssignmentAsync(archiveAddedItem).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var unAarchivedRemovedItem in result.Removed)
|
foreach (var unAarchivedRemovedItem in removedArchiveIds)
|
||||||
{
|
{
|
||||||
await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem);
|
await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,16 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
public async Task DownloadSearchResultMessageAsync(string messageId, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
|
public async Task DownloadSearchResultMessageAsync(string messageId, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(messageId) || assignedFolder == null) return;
|
||||||
|
|
||||||
|
// Online search can return the same message across repeated invocations/races.
|
||||||
|
// Guard before network+MIME download and before database insert.
|
||||||
|
var existing = await _outlookChangeProcessor.AreMailsExistsAsync([messageId]).ConfigureAwait(false);
|
||||||
|
if (existing.Contains(messageId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log.Information("Downloading search result message {messageId} for {Name} - {FolderName}", messageId, Account.Name, assignedFolder.FolderName);
|
Log.Information("Downloading search result message {messageId} for {Name} - {FolderName}", messageId, Account.Name, assignedFolder.FolderName);
|
||||||
|
|
||||||
// Outlook message handling was a little strange.
|
// Outlook message handling was a little strange.
|
||||||
@@ -292,7 +302,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await _outlookChangeProcessor.CreateMailRawAsync(Account, assignedFolder, package).ConfigureAwait(false);
|
// Use safe upsert path to avoid duplicate rows when message already exists.
|
||||||
|
await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1770,12 +1781,16 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
public override async Task<List<MailCopy>> OnlineSearchAsync(string queryText, List<IMailItemFolder> folders, CancellationToken cancellationToken = default)
|
public override async Task<List<MailCopy>> OnlineSearchAsync(string queryText, List<IMailItemFolder> folders, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
List<Message> messagesReturnedByApi = [];
|
var messagesById = new Dictionary<string, Message>(StringComparer.Ordinal);
|
||||||
|
|
||||||
// Perform search for each folder separately.
|
// Perform search for each folder separately.
|
||||||
if (folders?.Count > 0)
|
if (folders?.Count > 0)
|
||||||
{
|
{
|
||||||
var folderIds = folders.Select(a => a.RemoteFolderId);
|
var folderIds = folders
|
||||||
|
.Where(a => a != null && !string.IsNullOrWhiteSpace(a.RemoteFolderId))
|
||||||
|
.Select(a => a.RemoteFolderId)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var tasks = folderIds.Select(async folderId =>
|
var tasks = folderIds.Select(async folderId =>
|
||||||
{
|
{
|
||||||
@@ -1785,15 +1800,19 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
requestConfig.QueryParameters.Search = $"\"{queryText}\"";
|
requestConfig.QueryParameters.Search = $"\"{queryText}\"";
|
||||||
requestConfig.QueryParameters.Select = ["Id, ParentFolderId"];
|
requestConfig.QueryParameters.Select = ["Id, ParentFolderId"];
|
||||||
requestConfig.QueryParameters.Top = 1000;
|
requestConfig.QueryParameters.Top = 1000;
|
||||||
});
|
}, cancellationToken);
|
||||||
|
|
||||||
var result = await mailQuery;
|
var result = await mailQuery;
|
||||||
|
|
||||||
if (result?.Value != null)
|
if (result?.Value != null)
|
||||||
{
|
{
|
||||||
lock (messagesReturnedByApi)
|
lock (messagesById)
|
||||||
{
|
{
|
||||||
messagesReturnedByApi.AddRange(result.Value);
|
foreach (var message in result.Value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message?.Id)) continue;
|
||||||
|
messagesById[message.Id] = message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1815,22 +1834,24 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
if (result?.Value != null)
|
if (result?.Value != null)
|
||||||
{
|
{
|
||||||
messagesReturnedByApi.AddRange(result.Value);
|
foreach (var message in result.Value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message?.Id)) continue;
|
||||||
|
messagesById[message.Id] = message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messagesReturnedByApi.Count == 0) return [];
|
if (messagesById.Count == 0) return [];
|
||||||
|
|
||||||
var localFolders = (await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false))
|
var localFolders = (await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false))
|
||||||
.ToDictionary(x => x.RemoteFolderId);
|
.ToDictionary(x => x.RemoteFolderId);
|
||||||
|
|
||||||
var messagesDictionary = messagesReturnedByApi.ToDictionary(a => a.Id);
|
|
||||||
|
|
||||||
// Contains a list of message ids that potentially can be downloaded.
|
// Contains a list of message ids that potentially can be downloaded.
|
||||||
List<string> messageIdsWithKnownFolder = [];
|
var messageIdsWithKnownFolder = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
// Validate that all messages are in a known folder.
|
// Validate that all messages are in a known folder.
|
||||||
foreach (var message in messagesReturnedByApi)
|
foreach (var message in messagesById.Values)
|
||||||
{
|
{
|
||||||
if (!localFolders.ContainsKey(message.ParentFolderId))
|
if (!localFolders.ContainsKey(message.ParentFolderId))
|
||||||
{
|
{
|
||||||
@@ -1841,13 +1862,18 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
messageIdsWithKnownFolder.Add(message.Id);
|
messageIdsWithKnownFolder.Add(message.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (messageIdsWithKnownFolder.Count == 0) return [];
|
||||||
|
|
||||||
var locallyExistingMails = await _outlookChangeProcessor.AreMailsExistsAsync(messageIdsWithKnownFolder).ConfigureAwait(false);
|
var locallyExistingMails = await _outlookChangeProcessor.AreMailsExistsAsync(messageIdsWithKnownFolder).ConfigureAwait(false);
|
||||||
|
|
||||||
// Find messages that are not downloaded yet.
|
// Find messages that are not downloaded yet.
|
||||||
List<Message> messagesToDownload = [];
|
List<Message> messagesToDownload = [];
|
||||||
foreach (var id in messagesDictionary.Keys.Except(locallyExistingMails))
|
foreach (var id in messageIdsWithKnownFolder.Except(locallyExistingMails, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
messagesToDownload.Add(messagesDictionary[id]);
|
if (messagesById.TryGetValue(id, out var message))
|
||||||
|
{
|
||||||
|
messagesToDownload.Add(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var message in messagesToDownload)
|
foreach (var message in messagesToDownload)
|
||||||
|
|||||||
@@ -550,6 +550,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
{
|
{
|
||||||
IsOnlineSearchEnabled = false;
|
IsOnlineSearchEnabled = false;
|
||||||
AreSearchResultsOnline = false;
|
AreSearchResultsOnline = false;
|
||||||
|
HasNoOnlineSearchResult = false;
|
||||||
|
OnPropertyChanged(nameof(HasNoOnlineSearchResult));
|
||||||
IsInSearchMode = !string.IsNullOrEmpty(SearchQuery);
|
IsInSearchMode = !string.IsNullOrEmpty(SearchQuery);
|
||||||
|
|
||||||
if (IsInSearchMode)
|
if (IsInSearchMode)
|
||||||
@@ -619,6 +621,57 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsDraftOrSentFolder(MailCopy mailItem)
|
||||||
|
=> mailItem?.AssignedFolder?.SpecialFolderType is SpecialFolderType.Draft or SpecialFolderType.Sent;
|
||||||
|
|
||||||
|
private bool IsMailMatchingLocalSearch(MailCopy mailItem)
|
||||||
|
{
|
||||||
|
if (!IsInSearchMode) return true;
|
||||||
|
if (string.IsNullOrWhiteSpace(SearchQuery)) return true;
|
||||||
|
|
||||||
|
var query = SearchQuery.Trim();
|
||||||
|
|
||||||
|
return (!string.IsNullOrEmpty(mailItem.Subject) && mailItem.Subject.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrEmpty(mailItem.PreviewText) && mailItem.PreviewText.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrEmpty(mailItem.FromName) && mailItem.FromName.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrEmpty(mailItem.FromAddress) && mailItem.FromAddress.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldRemoveUpdatedMailFromCurrentList(MailCopy updatedMail)
|
||||||
|
{
|
||||||
|
if (ActiveFolder == null || updatedMail?.AssignedFolder == null) return true;
|
||||||
|
|
||||||
|
bool isFromDraftOrSentFolder = IsDraftOrSentFolder(updatedMail);
|
||||||
|
|
||||||
|
if (!isFromDraftOrSentFolder && !ActiveFolder.HandlingFolders.Any(a => a.Id == updatedMail.AssignedFolder.Id))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFromDraftOrSentFolder && !ThreadIdExistsInCollection(updatedMail))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldPreventItemAdd(updatedMail))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectedFolderPivot?.IsFocused is bool isFocused && updatedMail.IsFocused != isFocused)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Online search results are a server-provided snapshot. Keep current items stable.
|
||||||
|
if (IsInSearchMode && (IsOnlineSearchEnabled || AreSearchResultsOnline))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !IsMailMatchingLocalSearch(updatedMail);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void RemoveFirst()
|
public void RemoveFirst()
|
||||||
{
|
{
|
||||||
@@ -652,8 +705,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
if (!ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == addedMail.AssignedAccount.Id)) return;
|
if (!ActiveFolder.HandlingFolders.Any(a => a.MailAccountId == addedMail.AssignedAccount.Id)) return;
|
||||||
|
|
||||||
// Messages coming to sent or draft folder should only be inserted if their ThreadId exists in the collection.
|
// Messages coming to sent or draft folder should only be inserted if their ThreadId exists in the collection.
|
||||||
bool isFromDraftOrSentFolder = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
|
bool isFromDraftOrSentFolder = IsDraftOrSentFolder(addedMail);
|
||||||
addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
|
|
||||||
|
|
||||||
if (isFromDraftOrSentFolder)
|
if (isFromDraftOrSentFolder)
|
||||||
{
|
{
|
||||||
@@ -719,6 +771,20 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
if (ShouldPreventItemAdd(addedMail)) return;
|
if (ShouldPreventItemAdd(addedMail)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SelectedFolderPivot?.IsFocused is bool isFocused && addedMail.IsFocused != isFocused)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInSearchMode)
|
||||||
|
{
|
||||||
|
// Online search results are loaded from a dedicated query snapshot.
|
||||||
|
// Ignore live additions while that snapshot is active.
|
||||||
|
if (IsOnlineSearchEnabled || AreSearchResultsOnline) return;
|
||||||
|
|
||||||
|
if (!IsMailMatchingLocalSearch(addedMail)) return;
|
||||||
|
}
|
||||||
|
|
||||||
await listManipulationSemepahore.WaitAsync();
|
await listManipulationSemepahore.WaitAsync();
|
||||||
|
|
||||||
// AddAsync already handles UI threading internally, no need to wrap it
|
// AddAsync already handles UI threading internally, no need to wrap it
|
||||||
@@ -743,6 +809,17 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await listManipulationSemepahore.WaitAsync();
|
await listManipulationSemepahore.WaitAsync();
|
||||||
|
|
||||||
|
bool isItemListed = MailCollection.ContainsMailUniqueId(updatedMail.UniqueId);
|
||||||
|
if (!isItemListed) return;
|
||||||
|
|
||||||
|
if (ShouldRemoveUpdatedMailFromCurrentList(updatedMail))
|
||||||
|
{
|
||||||
|
await MailCollection.RemoveAsync(updatedMail);
|
||||||
|
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await MailCollection.UpdateMailCopy(updatedMail, source);
|
await MailCollection.UpdateMailCopy(updatedMail, source);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -890,6 +967,36 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
await InitializeFolderAsync();
|
await InitializeFolderAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<MailCopy>> PerformSynchronizerOnlineSearchAsync(string queryText,
|
||||||
|
IEnumerable<IMailItemFolder> handlingFolders,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (handlingFolders == null) return [];
|
||||||
|
|
||||||
|
var foldersByAccount = handlingFolders
|
||||||
|
.GroupBy(a => a.MailAccountId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (foldersByAccount.Count == 0) return [];
|
||||||
|
|
||||||
|
var searchTasks = foldersByAccount.Select(async groupedFolders =>
|
||||||
|
{
|
||||||
|
var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(groupedFolders.Key).ConfigureAwait(false);
|
||||||
|
if (synchronizer == null) return new List<MailCopy>();
|
||||||
|
|
||||||
|
var accountResults = await synchronizer.OnlineSearchAsync(queryText, groupedFolders.ToList(), cancellationToken).ConfigureAwait(false);
|
||||||
|
return accountResults ?? new List<MailCopy>();
|
||||||
|
});
|
||||||
|
|
||||||
|
var allResults = await Task.WhenAll(searchTasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return allResults
|
||||||
|
.SelectMany(a => a)
|
||||||
|
.GroupBy(a => a.UniqueId)
|
||||||
|
.Select(a => a.First())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task InitializeFolderAsync()
|
private async Task InitializeFolderAsync()
|
||||||
{
|
{
|
||||||
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
|
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
|
||||||
@@ -921,6 +1028,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
// Here items are sorted and filtered.
|
// Here items are sorted and filtered.
|
||||||
|
|
||||||
List<MailCopy> items = null;
|
List<MailCopy> items = null;
|
||||||
|
List<MailCopy> onlineSearchItems = null;
|
||||||
|
|
||||||
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
|
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
|
||||||
bool isDoingOnlineSearch = false;
|
bool isDoingOnlineSearch = false;
|
||||||
@@ -932,52 +1040,31 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
// Perform online search.
|
// Perform online search.
|
||||||
if (isDoingOnlineSearch)
|
if (isDoingOnlineSearch)
|
||||||
{
|
{
|
||||||
// TODO: Burak: Handle online search.
|
try
|
||||||
//WinoServerResponse<OnlineSearchResult> onlineSearchResult = null;
|
{
|
||||||
//string onlineSearchFailedMessage = null;
|
onlineSearchItems = await PerformSynchronizerOnlineSearchAsync(SearchQuery, ActiveFolder.HandlingFolders, cancellationToken).ConfigureAwait(false);
|
||||||
|
await ExecuteUIThread(() => { AreSearchResultsOnline = true; });
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to perform online search.");
|
||||||
|
|
||||||
//try
|
isDoingOnlineSearch = false;
|
||||||
//{
|
onlineSearchItems = null;
|
||||||
// var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId).ToList();
|
|
||||||
// var folders = ActiveFolder.HandlingFolders.ToList();
|
|
||||||
// var searchRequest = new OnlineSearchRequested(accountIds, SearchQuery, folders);
|
|
||||||
|
|
||||||
// onlineSearchResult = await _winoServerConnectionManager.GetResponseAsync<OnlineSearchResult, OnlineSearchRequested>(searchRequest, cancellationToken);
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
IsOnlineSearchEnabled = false;
|
||||||
|
AreSearchResultsOnline = false;
|
||||||
|
|
||||||
// if (onlineSearchResult.IsSuccess)
|
var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, ex.Message);
|
||||||
// {
|
_mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
|
||||||
// await ExecuteUIThread(() => { AreSearchResultsOnline = true; });
|
});
|
||||||
|
}
|
||||||
// onlineSearchItems = onlineSearchResult.Data.SearchResult;
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// onlineSearchFailedMessage = onlineSearchResult.Message;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//catch (OperationCanceledException)
|
|
||||||
//{
|
|
||||||
// throw;
|
|
||||||
//}
|
|
||||||
//catch (Exception ex)
|
|
||||||
//{
|
|
||||||
// Log.Warning(ex, "Failed to perform online search.");
|
|
||||||
// onlineSearchFailedMessage = ex.Message;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//if (onlineSearchResult != null && !onlineSearchResult.IsSuccess)
|
|
||||||
//{
|
|
||||||
// // Query or server error.
|
|
||||||
// var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchResult.Message);
|
|
||||||
// _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
|
|
||||||
|
|
||||||
//}
|
|
||||||
//else if (!string.IsNullOrEmpty(onlineSearchFailedMessage))
|
|
||||||
//{
|
|
||||||
// // Fatal error.
|
|
||||||
// var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchFailedMessage);
|
|
||||||
// _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,8 +1073,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
SelectedSortingOption.Type,
|
SelectedSortingOption.Type,
|
||||||
PreferencesService.IsThreadingEnabled,
|
PreferencesService.IsThreadingEnabled,
|
||||||
SelectedFolderPivot.IsFocused,
|
SelectedFolderPivot.IsFocused,
|
||||||
SearchQuery,
|
isDoingOnlineSearch ? string.Empty : SearchQuery,
|
||||||
MailCollection.MailCopyIdHashSet);
|
MailCollection.MailCopyIdHashSet,
|
||||||
|
onlineSearchItems);
|
||||||
|
|
||||||
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
|
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1003,6 +1091,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
|
HasNoOnlineSearchResult = isDoingOnlineSearch && items.Count == 0;
|
||||||
|
OnPropertyChanged(nameof(HasNoOnlineSearchResult));
|
||||||
|
|
||||||
if (isDoingSearch && !isDoingOnlineSearch)
|
if (isDoingSearch && !isDoingOnlineSearch)
|
||||||
{
|
{
|
||||||
IsOnlineSearchButtonVisible = true;
|
IsOnlineSearchButtonVisible = true;
|
||||||
@@ -1060,6 +1151,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
IsInSearchMode = false;
|
IsInSearchMode = false;
|
||||||
IsOnlineSearchButtonVisible = false;
|
IsOnlineSearchButtonVisible = false;
|
||||||
AreSearchResultsOnline = false;
|
AreSearchResultsOnline = false;
|
||||||
|
HasNoOnlineSearchResult = false;
|
||||||
|
OnPropertyChanged(nameof(HasNoOnlineSearchResult));
|
||||||
|
|
||||||
// Prepare Focused - Other or folder name tabs.
|
// Prepare Focused - Other or folder name tabs.
|
||||||
await UpdateFolderPivotsAsync();
|
await UpdateFolderPivotsAsync();
|
||||||
@@ -1093,6 +1186,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
SearchQuery = string.Empty;
|
SearchQuery = string.Empty;
|
||||||
IsInSearchMode = false;
|
IsInSearchMode = false;
|
||||||
IsOnlineSearchEnabled = false;
|
IsOnlineSearchEnabled = false;
|
||||||
|
HasNoOnlineSearchResult = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,67 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
return (sql.ToString(), parameters.ToArray());
|
return (sql.ToString(), parameters.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<MailCopy> ApplyOptionsToPreFetchedMails(MailListInitializationOptions options)
|
||||||
|
{
|
||||||
|
var allowedFolderIds = options.Folders.Select(f => f.Id).ToHashSet();
|
||||||
|
|
||||||
|
IEnumerable<MailCopy> query = options.PreFetchMailCopies
|
||||||
|
.Where(m => m != null && allowedFolderIds.Contains(m.FolderId))
|
||||||
|
.GroupBy(m => m.UniqueId)
|
||||||
|
.Select(g => g.First());
|
||||||
|
|
||||||
|
switch (options.FilterType)
|
||||||
|
{
|
||||||
|
case FilterOptionType.Unread:
|
||||||
|
query = query.Where(m => !m.IsRead);
|
||||||
|
break;
|
||||||
|
case FilterOptionType.Flagged:
|
||||||
|
query = query.Where(m => m.IsFlagged);
|
||||||
|
break;
|
||||||
|
case FilterOptionType.Files:
|
||||||
|
query = query.Where(m => m.HasAttachments);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IsFocusedOnly is bool isFocused)
|
||||||
|
{
|
||||||
|
query = query.Where(m => m.IsFocused == isFocused);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.SearchQuery))
|
||||||
|
{
|
||||||
|
var search = options.SearchQuery.Trim();
|
||||||
|
query = query.Where(m =>
|
||||||
|
(!string.IsNullOrEmpty(m.PreviewText) && m.PreviewText.Contains(search, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrEmpty(m.Subject) && m.Subject.Contains(search, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrEmpty(m.FromName) && m.FromName.Contains(search, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrEmpty(m.FromAddress) && m.FromAddress.Contains(search, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ExistingUniqueIds?.Any() ?? false)
|
||||||
|
{
|
||||||
|
query = query.Where(m => !options.ExistingUniqueIds.ContainsKey(m.UniqueId));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = options.SortingOptionType switch
|
||||||
|
{
|
||||||
|
SortingOptionType.Sender => query.OrderBy(m => m.FromName).ThenByDescending(m => m.CreationDate),
|
||||||
|
_ => query.OrderByDescending(m => m.CreationDate)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.Skip > 0)
|
||||||
|
{
|
||||||
|
query = query.Skip(options.Skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.Take > 0)
|
||||||
|
{
|
||||||
|
query = query.Take(options.Take);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<MailCopy>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
|
public async Task<List<MailCopy>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
List<MailCopy> mails = null;
|
List<MailCopy> mails = null;
|
||||||
@@ -237,7 +298,7 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
// If user performs an online search, mail copies are passed to options.
|
// If user performs an online search, mail copies are passed to options.
|
||||||
if (options.PreFetchMailCopies != null)
|
if (options.PreFetchMailCopies != null)
|
||||||
{
|
{
|
||||||
mails = options.PreFetchMailCopies;
|
mails = ApplyOptionsToPreFetchedMails(options);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1163,12 +1224,23 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
|
|
||||||
public async Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds)
|
public async Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds)
|
||||||
{
|
{
|
||||||
|
onlineArchiveMailIds ??= [];
|
||||||
|
|
||||||
var localArchiveMails = await Connection.Table<MailCopy>()
|
var localArchiveMails = await Connection.Table<MailCopy>()
|
||||||
.Where(a => a.FolderId == archiveFolderId)
|
.Where(a => a.FolderId == archiveFolderId)
|
||||||
.ToListAsync().ConfigureAwait(false);
|
.ToListAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var removedMails = localArchiveMails.Where(a => !onlineArchiveMailIds.Contains(a.Id)).Select(a => a.Id).Distinct().ToArray();
|
var onlineArchiveIdSet = onlineArchiveMailIds
|
||||||
var addedMails = onlineArchiveMailIds.Where(a => !localArchiveMails.Select(b => b.Id).Contains(a)).Distinct().ToArray();
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var localArchiveIdSet = localArchiveMails
|
||||||
|
.Select(a => a.Id)
|
||||||
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var removedMails = localArchiveIdSet.Except(onlineArchiveIdSet).ToArray();
|
||||||
|
var addedMails = onlineArchiveIdSet.Except(localArchiveIdSet).ToArray();
|
||||||
|
|
||||||
return new GmailArchiveComparisonResult(addedMails, removedMails);
|
return new GmailArchiveComparisonResult(addedMails, removedMails);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user