Online Search (#576)

* Very basic online search for gmail.

* Server side of handling offline search and listing part in listing page.

* Default search mode implementation and search UI improvements.

* Online search for Outlook.

* Very basic online search for gmail.

* Server side of handling offline search and listing part in listing page.

* Default search mode implementation and search UI improvements.

* Online search for Outlook.

* Online search for imap without downloading the messages yet. TODO

* Completing imap search.
This commit is contained in:
Burak Kaan Köse
2025-02-22 00:22:00 +01:00
committed by GitHub
parent 42b695854b
commit f61bcb621b
30 changed files with 900 additions and 209 deletions

View File

@@ -18,6 +18,10 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
[ObservableProperty]
private List<string> _appTerminationBehavior;
[ObservableProperty]
public partial List<string> SearchModes { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))]
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))]
@@ -38,6 +42,18 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
}
}
private string _selectedDefaultSearchMode;
public string SelectedDefaultSearchMode
{
get => _selectedDefaultSearchMode;
set
{
SetProperty(ref _selectedDefaultSearchMode, value);
PreferencesService.DefaultSearchMode = (SearchMode)SearchModes.IndexOf(value);
}
}
private readonly IMailDialogService _dialogService;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private readonly IStartupBehaviorService _startupBehaviorService;
@@ -61,7 +77,14 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
Translator.SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title // "Terminate"
];
SearchModes =
[
Translator.SettingsAppPreferences_SearchMode_Local,
Translator.SettingsAppPreferences_SearchMode_Online
];
SelectedAppTerminationBehavior = _appTerminationBehavior[(int)PreferencesService.ServerTerminationBehavior];
SelectedDefaultSearchMode = SearchModes[(int)PreferencesService.DefaultSearchMode];
}
[RelayCommand]

View File

@@ -22,6 +22,7 @@ using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Server;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Mail.ViewModels.Collections;
using Wino.Mail.ViewModels.Data;
@@ -71,12 +72,14 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public IThemeService ThemeService { get; }
private readonly IAccountService _accountService;
private readonly IMailDialogService _mailDialogService;
private readonly IMailService _mailService;
private readonly IFolderService _folderService;
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
private readonly IContextMenuItemService _contextMenuItemService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService;
private readonly IWinoLogger _winoLogger;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private MailItemViewModel _activeMailItem;
@@ -100,7 +103,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private bool isMultiSelectionModeEnabled;
[ObservableProperty]
private string searchQuery;
public partial string SearchQuery { get; set; }
[ObservableProperty]
private FilterOption _selectedFilterOption;
@@ -109,7 +112,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Indicates state when folder is initializing. It can happen after folder navigation, search or filter change applied or loading more items.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEmpty))]
[NotifyPropertyChangedFor(nameof(IsCriteriaFailed))]
[NotifyPropertyChangedFor(nameof(IsFolderEmpty))]
[NotifyPropertyChangedFor(nameof(IsProgressRing))]
private bool isInitializingFolder;
@@ -147,6 +149,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public MailListPageViewModel(IMailDialogService dialogService,
INavigationService navigationService,
IAccountService accountService,
IMailDialogService mailDialogService,
IMailService mailService,
IStatePersistanceService statePersistenceService,
IFolderService folderService,
@@ -156,14 +159,17 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IKeyPressService keyPressService,
IPreferencesService preferencesService,
IThemeService themeService,
IWinoLogger winoLogger,
IWinoServerConnectionManager winoServerConnectionManager)
{
PreferencesService = preferencesService;
ThemeService = themeService;
_winoLogger = winoLogger;
_winoServerConnectionManager = winoServerConnectionManager;
StatePersistenceService = statePersistenceService;
NavigationService = navigationService;
_accountService = accountService;
_mailDialogService = mailDialogService;
_mailService = mailService;
_folderService = folderService;
_threadingStrategyProvider = threadingStrategyProvider;
@@ -254,6 +260,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
public string SelectedMessageText => HasSelectedItems ? string.Format(Translator.MailsSelected, SelectedItemCount) : Translator.NoMailSelected;
/// <summary>
/// Indicates current state of the mail list. Doesn't matter it's loading or no.
/// </summary>
@@ -263,11 +270,21 @@ public partial class MailListPageViewModel : MailBaseViewModel,
/// Progress ring only should be visible when the folder is initializing and there are no items. We don't need to show it when there are items.
/// </summary>
public bool IsProgressRing => IsInitializingFolder && IsEmpty;
private bool isFilters => IsInSearchMode || SelectedFilterOption.Type != FilterOptionType.All;
public bool IsCriteriaFailed => !IsInitializingFolder && IsEmpty && isFilters;
public bool IsFolderEmpty => !IsInitializingFolder && IsEmpty && !isFilters;
public bool IsFolderEmpty => !IsInitializingFolder && IsEmpty;
public bool IsInSearchMode { get; set; }
public bool HasNoOnlineSearchResult { get; private set; }
[ObservableProperty]
public partial bool IsInSearchMode { get; set; }
[ObservableProperty]
public partial bool IsOnlineSearchButtonVisible { get; set; }
[ObservableProperty]
public partial bool IsOnlineSearchEnabled { get; set; }
[ObservableProperty]
public partial bool AreSearchResultsOnline { get; set; }
#endregion
@@ -333,7 +350,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private void NotifyItemFoundState()
{
OnPropertyChanged(nameof(IsEmpty));
OnPropertyChanged(nameof(IsCriteriaFailed));
OnPropertyChanged(nameof(IsFolderEmpty));
}
@@ -378,41 +394,52 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private async Task UpdateFolderPivotsAsync()
{
if (ActiveFolder == null) return;
PivotFolders.Clear();
SelectedFolderPivot = null;
if (ActiveFolder == null) return;
// Merged folders don't support focused feature.
if (ActiveFolder is IMergedAccountFolderMenuItem)
if (IsInSearchMode)
{
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
var isFocused = SelectedFolderPivot?.IsFocused;
PivotFolders.Add(new FolderPivotViewModel(Translator.SearchPivotName, isFocused));
}
else if (ActiveFolder is IFolderMenuItem singleFolderMenuItem)
else
{
var parentAccount = singleFolderMenuItem.ParentAccount;
// Merged folders don't support focused feature.
bool isFocusedInboxEnabled = await _accountService.IsAccountFocusedEnabledAsync(parentAccount.Id);
bool isInboxFolder = ActiveFolder.SpecialFolderType == SpecialFolderType.Inbox;
// Folder supports Focused - Other
if (isInboxFolder && isFocusedInboxEnabled)
if (ActiveFolder is IMergedAccountFolderMenuItem)
{
// Can be passed as empty string. Focused - Other will be used regardless.
var focusedItem = new FolderPivotViewModel(string.Empty, true);
var otherItem = new FolderPivotViewModel(string.Empty, false);
PivotFolders.Add(focusedItem);
PivotFolders.Add(otherItem);
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
}
else
else if (ActiveFolder is IFolderMenuItem singleFolderMenuItem)
{
// If the account and folder doesn't support focused feature, just add itself.
PivotFolders.Add(new FolderPivotViewModel(singleFolderMenuItem.FolderName, null));
var parentAccount = singleFolderMenuItem.ParentAccount;
bool isFocusedInboxEnabled = await _accountService.IsAccountFocusedEnabledAsync(parentAccount.Id);
bool isInboxFolder = ActiveFolder.SpecialFolderType == SpecialFolderType.Inbox;
// Folder supports Focused - Other
if (isInboxFolder && isFocusedInboxEnabled)
{
// Can be passed as empty string. Focused - Other will be used regardless.
var focusedItem = new FolderPivotViewModel(string.Empty, true);
var otherItem = new FolderPivotViewModel(string.Empty, false);
PivotFolders.Add(focusedItem);
PivotFolders.Add(otherItem);
}
else
{
// If the account and folder doesn't support focused feature, just add itself.
PivotFolders.Add(new FolderPivotViewModel(singleFolderMenuItem.FolderName, null));
}
}
}
// This will trigger refresh.
SelectedFolderPivot = PivotFolders.FirstOrDefault();
}
@@ -512,33 +539,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
public async Task PerformSearchAsync()
{
if (string.IsNullOrEmpty(SearchQuery) && IsInSearchMode)
IsOnlineSearchEnabled = false;
AreSearchResultsOnline = false;
IsInSearchMode = !string.IsNullOrEmpty(SearchQuery);
if (IsInSearchMode)
{
await UpdateFolderPivotsAsync();
IsInSearchMode = false;
await InitializeFolderAsync();
IsOnlineSearchButtonVisible = false;
}
if (!string.IsNullOrEmpty(SearchQuery))
{
IsInSearchMode = true;
CreateSearchPivot();
}
void CreateSearchPivot()
{
PivotFolders.Clear();
var isFocused = SelectedFolderPivot?.IsFocused;
SelectedFolderPivot = null;
if (ActiveFolder == null) return;
PivotFolders.Add(new FolderPivotViewModel(Translator.SearchPivotName, isFocused));
// This will trigger refresh.
SelectedFolderPivot = PivotFolders.FirstOrDefault();
}
await UpdateFolderPivotsAsync();
}
[RelayCommand]
@@ -555,7 +565,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
private async Task LoadMoreItemsAsync()
{
if (IsInitializingFolder) return;
if (IsInitializingFolder || IsOnlineSearchEnabled) return;
await ExecuteUIThread(() => { IsInitializingFolder = true; });
@@ -763,6 +773,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
[RelayCommand]
private async Task PerformOnlineSearchAsync()
{
IsOnlineSearchButtonVisible = false;
IsOnlineSearchEnabled = true;
await InitializeFolderAsync();
}
private async Task InitializeFolderAsync()
{
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
@@ -783,14 +802,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Folder is changed during initialization.
// Just cancel the existing one and wait for new initialization.
//if (listManipulationSemepahore.CurrentCount == 0)
//{
// Debug.WriteLine("Canceling initialization of mails.");
// listManipulationCancellationTokenSource.Cancel();
// listManipulationCancellationTokenSource.Token.ThrowIfCancellationRequested();
//}
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
listManipulationCancellationTokenSource.Cancel();
@@ -812,15 +823,77 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Here items are sorted and filtered.
List<IMailItem> items = null;
List<MailCopy> onlineSearchItems = null;
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
bool isDoingOnlineSearch = false;
if (isDoingSearch)
{
isDoingOnlineSearch = PreferencesService.DefaultSearchMode == SearchMode.Online || IsOnlineSearchEnabled;
// Perform online search.
if (isDoingOnlineSearch)
{
WinoServerResponse<OnlineSearchResult> onlineSearchResult = null;
string onlineSearchFailedMessage = null;
try
{
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);
if (onlineSearchResult.IsSuccess)
{
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);
}
}
}
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
SelectedFilterOption.Type,
SelectedSortingOption.Type,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
SearchQuery,
MailCollection.MailCopyIdHashSet);
MailCollection.MailCopyIdHashSet,
onlineSearchItems);
var items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
@@ -830,7 +903,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
var viewModels = PrepareMailViewModels(items);
await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, true); });
await ExecuteUIThread(() =>
{
MailCollection.AddRange(viewModels, true);
if (isDoingSearch && !isDoingOnlineSearch)
{
IsOnlineSearchButtonVisible = true;
}
});
}
}
catch (OperationCanceledException)
@@ -887,6 +968,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Notify change for archive-unarchive app bar button.
OnPropertyChanged(nameof(IsArchiveSpecialFolder));
IsInSearchMode = false;
IsOnlineSearchButtonVisible = false;
AreSearchResultsOnline = false;
// Prepare Focused - Other or folder name tabs.
await UpdateFolderPivotsAsync();
@@ -918,6 +1003,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
SelectedSortingOption = SortingOptions[0];
SearchQuery = string.Empty;
IsInSearchMode = false;
IsOnlineSearchEnabled = false;
}
}