458 lines
14 KiB
C#
458 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Entities.Shared;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.Navigation;
|
|
|
|
namespace Wino.Mail.ViewModels;
|
|
|
|
public partial class ContactsPageViewModel : MailBaseViewModel
|
|
{
|
|
private const int ContactPageSize = 50;
|
|
|
|
private readonly IContactService _contactService;
|
|
private readonly IMailDialogService _dialogService;
|
|
|
|
private CancellationTokenSource _searchDebounceCancellationTokenSource;
|
|
private int _currentOffset = 0;
|
|
private int _currentQueryVersion = 0;
|
|
|
|
[ObservableProperty]
|
|
public partial string SearchQuery { get; set; } = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
|
[NotifyPropertyChangedFor(nameof(IsEmpty))]
|
|
public partial bool IsLoading { get; set; } = false;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
|
public partial bool IsLoadingMore { get; set; } = false;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
|
public partial bool HasMoreContacts { get; set; } = false;
|
|
|
|
[ObservableProperty]
|
|
public partial bool IsSelectionMode { get; set; } = false;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(DeleteSelectedContactsCommand))]
|
|
public partial int SelectedContactsCount { get; set; } = 0;
|
|
|
|
[ObservableProperty]
|
|
public partial int TotalContactsCount { get; set; } = 0;
|
|
|
|
public bool IsEmpty => !IsLoading && Contacts.Count == 0;
|
|
public bool CanLoadMoreContacts => HasMoreContacts && !IsLoading && !IsLoadingMore;
|
|
public bool CanDeleteSelectedContacts => SelectedContactsCount > 0;
|
|
|
|
public ObservableCollection<AccountContact> Contacts { get; } = new();
|
|
public ObservableCollection<AccountContact> SelectedContacts { get; } = new();
|
|
|
|
public ContactsPageViewModel(IContactService contactService, IMailDialogService dialogService)
|
|
{
|
|
_contactService = contactService;
|
|
_dialogService = dialogService;
|
|
|
|
Contacts.CollectionChanged += ContactsCollectionChanged;
|
|
}
|
|
|
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
{
|
|
base.OnNavigatedTo(mode, parameters);
|
|
|
|
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
|
SelectedContacts.CollectionChanged += SelectedContactsChanged;
|
|
|
|
await ReloadContactsAsync();
|
|
}
|
|
|
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
|
{
|
|
base.OnNavigatedFrom(mode, parameters);
|
|
|
|
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
|
|
|
_searchDebounceCancellationTokenSource?.Cancel();
|
|
_searchDebounceCancellationTokenSource?.Dispose();
|
|
_searchDebounceCancellationTokenSource = null;
|
|
}
|
|
|
|
private async void SelectedContactsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
=> await ExecuteUIThread(() => { SelectedContactsCount = SelectedContacts.Count; });
|
|
|
|
private async void ContactsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
=> await ExecuteUIThread(() => { OnPropertyChanged(nameof(IsEmpty)); });
|
|
|
|
[RelayCommand]
|
|
private async Task ReloadContactsAsync()
|
|
{
|
|
var queryVersion = ++_currentQueryVersion;
|
|
_currentOffset = 0;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
HasMoreContacts = false;
|
|
Contacts.Clear();
|
|
SelectedContacts.Clear();
|
|
});
|
|
|
|
await LoadContactsPageAsync(queryVersion, reset: true);
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanLoadMoreContacts))]
|
|
private async Task LoadMoreContactsAsync()
|
|
{
|
|
await LoadContactsPageAsync(_currentQueryVersion, reset: false);
|
|
}
|
|
|
|
private async Task LoadContactsPageAsync(int queryVersion, bool reset)
|
|
{
|
|
if (IsLoading || IsLoadingMore)
|
|
return;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
if (reset)
|
|
IsLoading = true;
|
|
else
|
|
IsLoadingMore = true;
|
|
});
|
|
|
|
try
|
|
{
|
|
var searchQuery = string.IsNullOrWhiteSpace(SearchQuery) ? null : SearchQuery.Trim();
|
|
var page = await _contactService.GetContactsPageAsync(
|
|
_currentOffset,
|
|
ContactPageSize,
|
|
searchQuery,
|
|
excludeRootContacts: true).ConfigureAwait(false);
|
|
|
|
if (queryVersion != _currentQueryVersion)
|
|
return;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
if (reset)
|
|
{
|
|
Contacts.Clear();
|
|
}
|
|
|
|
foreach (var contact in page.Contacts)
|
|
{
|
|
Contacts.Add(contact);
|
|
}
|
|
|
|
TotalContactsCount = page.TotalCount;
|
|
HasMoreContacts = page.HasMore;
|
|
_currentOffset = Contacts.Count;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (queryVersion != _currentQueryVersion)
|
|
return;
|
|
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
string.Format(Translator.ContactInfoBar_FailedToLoadContacts, ex.Message),
|
|
InfoBarMessageType.Error);
|
|
}
|
|
finally
|
|
{
|
|
if (queryVersion == _currentQueryVersion)
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
if (reset)
|
|
IsLoading = false;
|
|
else
|
|
IsLoadingMore = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task AddContactAsync()
|
|
{
|
|
var result = await _dialogService.ShowEditContactDialogAsync(null);
|
|
|
|
if (result == null) return;
|
|
|
|
try
|
|
{
|
|
var newContact = await _contactService.CreateNewContactAsync(result.Address, result.Name);
|
|
|
|
if (!string.IsNullOrEmpty(result.Base64ContactPicture))
|
|
{
|
|
newContact.Base64ContactPicture = result.Base64ContactPicture;
|
|
await _contactService.UpdateContactAsync(newContact);
|
|
}
|
|
|
|
await ReloadContactsAsync();
|
|
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_SuccessTitle,
|
|
Translator.ContactInfoBar_ContactAdded,
|
|
InfoBarMessageType.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
string.Format(Translator.ContactInfoBar_FailedToAddContact, ex.Message),
|
|
InfoBarMessageType.Error);
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task EditContactAsync(AccountContact contact)
|
|
{
|
|
if (contact == null) return;
|
|
|
|
var result = await _dialogService.ShowEditContactDialogAsync(contact);
|
|
|
|
if (result == null) return;
|
|
|
|
try
|
|
{
|
|
contact.Name = result.Name;
|
|
contact.Base64ContactPicture = result.Base64ContactPicture;
|
|
contact.IsOverridden = result.IsOverridden;
|
|
|
|
await _contactService.UpdateContactAsync(contact);
|
|
await ReloadContactsAsync();
|
|
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_SuccessTitle,
|
|
Translator.ContactInfoBar_ContactUpdated,
|
|
InfoBarMessageType.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
string.Format(Translator.ContactInfoBar_FailedToUpdateContact, ex.Message),
|
|
InfoBarMessageType.Error);
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task DeleteContactAsync(AccountContact contact)
|
|
{
|
|
if (contact == null || contact.IsRootContact)
|
|
{
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_WarningTitle,
|
|
Translator.ContactInfoBar_CannotDeleteRoot,
|
|
InfoBarMessageType.Warning);
|
|
return;
|
|
}
|
|
|
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
|
string.Format(Translator.ContactConfirmDialog_DeleteMessage, contact.Name ?? contact.Address),
|
|
Translator.ContactConfirmDialog_DeleteTitle,
|
|
Translator.ContactConfirmDialog_DeleteButton);
|
|
|
|
if (confirmed)
|
|
{
|
|
await DeleteContactsInternalAsync(new[] { contact });
|
|
}
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanDeleteSelectedContacts))]
|
|
private async Task DeleteSelectedContactsAsync()
|
|
{
|
|
if (SelectedContacts.Count == 0) return;
|
|
|
|
var deletableContacts = SelectedContacts
|
|
.Where(c => c != null && !c.IsRootContact)
|
|
.GroupBy(c => c.Address, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => g.First())
|
|
.ToList();
|
|
|
|
if (deletableContacts.Count == 0)
|
|
{
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_WarningTitle,
|
|
Translator.ContactInfoBar_CannotDeleteRoot,
|
|
InfoBarMessageType.Warning);
|
|
return;
|
|
}
|
|
|
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
|
string.Format(Translator.ContactConfirmDialog_DeleteMultipleMessage, deletableContacts.Count),
|
|
Translator.ContactConfirmDialog_DeleteTitle,
|
|
Translator.ContactConfirmDialog_DeleteButton);
|
|
|
|
if (confirmed)
|
|
{
|
|
await DeleteContactsInternalAsync(deletableContacts);
|
|
}
|
|
}
|
|
|
|
private async Task DeleteContactsInternalAsync(IEnumerable<AccountContact> contactsToDelete)
|
|
{
|
|
try
|
|
{
|
|
var addresses = contactsToDelete
|
|
.Select(c => c.Address)
|
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
if (addresses.Count == 0) return;
|
|
|
|
await _contactService.DeleteContactsAsync(addresses);
|
|
await ReloadContactsAsync();
|
|
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_SuccessTitle,
|
|
Translator.ContactInfoBar_ContactsDeleted,
|
|
InfoBarMessageType.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
string.Format(Translator.ContactInfoBar_FailedToDeleteContacts, ex.Message),
|
|
InfoBarMessageType.Error);
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ToggleSelection()
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
IsSelectionMode = !IsSelectionMode;
|
|
|
|
if (!IsSelectionMode)
|
|
{
|
|
SelectedContacts.Clear();
|
|
}
|
|
});
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task SelectAllContacts()
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
SelectedContacts.Clear();
|
|
|
|
foreach (var contact in Contacts)
|
|
{
|
|
SelectedContacts.Add(contact);
|
|
}
|
|
});
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ClearSelection()
|
|
{
|
|
await ExecuteUIThread(() => { SelectedContacts.Clear(); });
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task PickContactPhotoAsync(AccountContact contact)
|
|
{
|
|
if (contact == null) return;
|
|
|
|
try
|
|
{
|
|
var files = await _dialogService.PickFilesAsync(".png", ".jpg", ".jpeg");
|
|
|
|
if (files?.Any() == true)
|
|
{
|
|
var file = files.First();
|
|
var base64Image = Convert.ToBase64String(file.Data);
|
|
|
|
contact.Base64ContactPicture = base64Image;
|
|
await _contactService.UpdateContactAsync(contact);
|
|
await RefreshContactInUiAsync(contact);
|
|
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_SuccessTitle,
|
|
Translator.ContactInfoBar_ContactPhotoUpdated,
|
|
InfoBarMessageType.Success);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_dialogService.InfoBarMessage(
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
string.Format(Translator.ContactInfoBar_FailedToUpdatePhoto, ex.Message),
|
|
InfoBarMessageType.Error);
|
|
}
|
|
}
|
|
|
|
private async Task RefreshContactInUiAsync(AccountContact contact)
|
|
{
|
|
if (contact == null || string.IsNullOrWhiteSpace(contact.Address))
|
|
return;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
ReplaceContactByAddress(Contacts, contact);
|
|
ReplaceContactByAddress(SelectedContacts, contact);
|
|
});
|
|
}
|
|
|
|
private static void ReplaceContactByAddress(ObservableCollection<AccountContact> source, AccountContact updatedContact)
|
|
{
|
|
var index = source
|
|
.Select((item, i) => new { item, i })
|
|
.FirstOrDefault(x => string.Equals(x.item.Address, updatedContact.Address, StringComparison.OrdinalIgnoreCase))
|
|
?.i ?? -1;
|
|
|
|
if (index < 0) return;
|
|
|
|
source[index] = CloneContact(updatedContact);
|
|
}
|
|
|
|
private static AccountContact CloneContact(AccountContact contact)
|
|
=> new()
|
|
{
|
|
Address = contact.Address,
|
|
Name = contact.Name,
|
|
Base64ContactPicture = contact.Base64ContactPicture,
|
|
IsRootContact = contact.IsRootContact,
|
|
IsOverridden = contact.IsOverridden
|
|
};
|
|
|
|
partial void OnSearchQueryChanged(string value)
|
|
{
|
|
DebounceSearchAndReload();
|
|
}
|
|
|
|
private async void DebounceSearchAndReload()
|
|
{
|
|
_searchDebounceCancellationTokenSource?.Cancel();
|
|
_searchDebounceCancellationTokenSource?.Dispose();
|
|
|
|
_searchDebounceCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
try
|
|
{
|
|
await Task.Delay(250, _searchDebounceCancellationTokenSource.Token);
|
|
await ReloadContactsAsync();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Ignore stale search input.
|
|
}
|
|
}
|
|
}
|