2025-10-29 19:35:04 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
2026-02-09 22:39:30 +01:00
|
|
|
using System.Collections.Specialized;
|
2025-10-29 19:35:04 +01:00
|
|
|
using System.Linq;
|
2026-02-09 22:39:30 +01:00
|
|
|
using System.Threading;
|
2025-10-29 19:35:04 +01:00
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-03-12 14:55:07 +01:00
|
|
|
using CommunityToolkit.Mvvm.Messaging;
|
2026-02-09 22:39:30 +01:00
|
|
|
using Wino.Core.Domain;
|
2025-10-29 19:35:04 +01:00
|
|
|
using Wino.Core.Domain.Entities.Shared;
|
|
|
|
|
using Wino.Core.Domain.Enums;
|
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
|
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2026-03-06 12:31:37 +01:00
|
|
|
using Wino.Mail.ViewModels.Data;
|
2026-03-12 14:55:07 +01:00
|
|
|
using Wino.Messaging.Client.Contacts;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
|
|
|
|
namespace Wino.Mail.ViewModels;
|
|
|
|
|
|
2026-03-12 14:55:07 +01:00
|
|
|
public partial class ContactsPageViewModel : MailBaseViewModel,
|
|
|
|
|
IRecipient<NewContactRequested>
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
private const int ContactPageSize = 50;
|
|
|
|
|
|
2025-10-29 19:35:04 +01:00
|
|
|
private readonly IContactService _contactService;
|
|
|
|
|
private readonly IMailDialogService _dialogService;
|
2026-03-07 11:43:56 +01:00
|
|
|
private readonly IContactPictureFileService _contactPictureFileService;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private CancellationTokenSource _searchDebounceCancellationTokenSource;
|
|
|
|
|
private int _currentOffset = 0;
|
|
|
|
|
private int _currentQueryVersion = 0;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
public partial string SearchQuery { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-02-09 22:39:30 +01:00
|
|
|
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsEmpty))]
|
2025-10-29 19:35:04 +01:00
|
|
|
public partial bool IsLoading { get; set; } = false;
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
|
|
|
|
public partial bool IsLoadingMore { get; set; } = false;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
|
|
|
|
public partial bool HasMoreContacts { get; set; } = false;
|
|
|
|
|
|
2025-10-29 19:35:04 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
public partial bool IsSelectionMode { get; set; } = false;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-02-09 22:39:30 +01:00
|
|
|
[NotifyCanExecuteChangedFor(nameof(DeleteSelectedContactsCommand))]
|
2025-10-29 19:35:04 +01:00
|
|
|
public partial int SelectedContactsCount { get; set; } = 0;
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
[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;
|
|
|
|
|
|
2026-03-06 12:31:37 +01:00
|
|
|
public ObservableCollection<AccountContactViewModel> Contacts { get; } = new();
|
|
|
|
|
public ObservableCollection<AccountContactViewModel> SelectedContacts { get; } = new();
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-03-07 11:43:56 +01:00
|
|
|
public ContactsPageViewModel(IContactService contactService, IMailDialogService dialogService, IContactPictureFileService contactPictureFileService)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
|
|
|
|
_contactService = contactService;
|
|
|
|
|
_dialogService = dialogService;
|
2026-03-07 11:43:56 +01:00
|
|
|
_contactPictureFileService = contactPictureFileService;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
Contacts.CollectionChanged += ContactsCollectionChanged;
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedTo(mode, parameters);
|
|
|
|
|
|
|
|
|
|
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
|
|
|
|
SelectedContacts.CollectionChanged += SelectedContactsChanged;
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await ReloadContactsAsync();
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedFrom(mode, parameters);
|
|
|
|
|
|
|
|
|
|
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
2026-02-09 22:39:30 +01:00
|
|
|
|
|
|
|
|
_searchDebounceCancellationTokenSource?.Cancel();
|
|
|
|
|
_searchDebounceCancellationTokenSource?.Dispose();
|
|
|
|
|
_searchDebounceCancellationTokenSource = null;
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
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)); });
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-03-12 14:55:07 +01:00
|
|
|
void IRecipient<NewContactRequested>.Receive(NewContactRequested message)
|
|
|
|
|
=> _ = AddContactAsync();
|
|
|
|
|
|
2025-10-29 19:35:04 +01:00
|
|
|
[RelayCommand]
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task ReloadContactsAsync()
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
var queryVersion = ++_currentQueryVersion;
|
|
|
|
|
_currentOffset = 0;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
HasMoreContacts = false;
|
|
|
|
|
Contacts.Clear();
|
|
|
|
|
SelectedContacts.Clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await LoadContactsPageAsync(queryVersion, reset: true);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
[RelayCommand(CanExecute = nameof(CanLoadMoreContacts))]
|
|
|
|
|
private async Task LoadMoreContactsAsync()
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await LoadContactsPageAsync(_currentQueryVersion, reset: false);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task LoadContactsPageAsync(int queryVersion, bool reset)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (IsLoading || IsLoadingMore)
|
|
|
|
|
return;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (reset)
|
|
|
|
|
IsLoading = true;
|
|
|
|
|
else
|
|
|
|
|
IsLoadingMore = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
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)
|
|
|
|
|
{
|
2026-03-06 12:31:37 +01:00
|
|
|
Contacts.Add(new AccountContactViewModel(contact));
|
2026-02-09 22:39:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TotalContactsCount = page.TotalCount;
|
|
|
|
|
HasMoreContacts = page.HasMore;
|
|
|
|
|
_currentOffset = Contacts.Count;
|
|
|
|
|
});
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
if (queryVersion != _currentQueryVersion)
|
|
|
|
|
return;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
|
|
|
string.Format(Translator.ContactInfoBar_FailedToLoadContacts, ex.Message),
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (queryVersion == _currentQueryVersion)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
if (reset)
|
|
|
|
|
IsLoading = false;
|
|
|
|
|
else
|
|
|
|
|
IsLoadingMore = false;
|
|
|
|
|
});
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
}
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task AddContactAsync()
|
|
|
|
|
{
|
|
|
|
|
var result = await _dialogService.ShowEditContactDialogAsync(null);
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (result == null) return;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var newContact = await _contactService.CreateNewContactAsync(result.Address, result.Name);
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-03-07 11:43:56 +01:00
|
|
|
if (result.ContactPictureFileId.HasValue)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-03-07 11:43:56 +01:00
|
|
|
newContact.ContactPictureFileId = result.ContactPictureFileId;
|
2026-02-09 22:39:30 +01:00
|
|
|
await _contactService.UpdateContactAsync(newContact);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
|
|
|
|
|
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);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 14:55:07 +01:00
|
|
|
protected override void RegisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
base.RegisterRecipients();
|
|
|
|
|
|
|
|
|
|
Messenger.Register<NewContactRequested>(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void UnregisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
base.UnregisterRecipients();
|
|
|
|
|
|
|
|
|
|
Messenger.Unregister<NewContactRequested>(this);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-29 19:35:04 +01:00
|
|
|
[RelayCommand]
|
2026-03-06 12:31:37 +01:00
|
|
|
private async Task EditContactAsync(AccountContactViewModel contactViewModel)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-03-06 12:31:37 +01:00
|
|
|
var contact = contactViewModel?.SourceContact;
|
2025-10-29 19:35:04 +01:00
|
|
|
if (contact == null) return;
|
|
|
|
|
|
|
|
|
|
var result = await _dialogService.ShowEditContactDialogAsync(contact);
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (result == null) return;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
contact.Name = result.Name;
|
2026-03-07 11:43:56 +01:00
|
|
|
contact.ContactPictureFileId = result.ContactPictureFileId;
|
2026-02-09 22:39:30 +01:00
|
|
|
contact.IsOverridden = result.IsOverridden;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await _contactService.UpdateContactAsync(contact);
|
|
|
|
|
await ReloadContactsAsync();
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
_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);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-03-06 12:31:37 +01:00
|
|
|
private async Task DeleteContactAsync(AccountContactViewModel contactViewModel)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-03-06 12:31:37 +01:00
|
|
|
var contact = contactViewModel?.SourceContact;
|
2025-10-29 19:35:04 +01:00
|
|
|
if (contact == null || contact.IsRootContact)
|
|
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_WarningTitle,
|
|
|
|
|
Translator.ContactInfoBar_CannotDeleteRoot,
|
|
|
|
|
InfoBarMessageType.Warning);
|
2025-10-29 19:35:04 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
|
|
|
|
string.Format(Translator.ContactConfirmDialog_DeleteMessage, contact.Name ?? contact.Address),
|
|
|
|
|
Translator.ContactConfirmDialog_DeleteTitle,
|
|
|
|
|
Translator.ContactConfirmDialog_DeleteButton);
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (confirmed)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
|
|
|
|
await DeleteContactsInternalAsync(new[] { contact });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
[RelayCommand(CanExecute = nameof(CanDeleteSelectedContacts))]
|
2025-10-29 19:35:04 +01:00
|
|
|
private async Task DeleteSelectedContactsAsync()
|
|
|
|
|
{
|
|
|
|
|
if (SelectedContacts.Count == 0) return;
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var deletableContacts = SelectedContacts
|
2026-03-06 12:31:37 +01:00
|
|
|
.Select(c => c?.SourceContact)
|
2026-02-09 22:39:30 +01:00
|
|
|
.Where(c => c != null && !c.IsRootContact)
|
|
|
|
|
.GroupBy(c => c.Address, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
.Select(g => g.First())
|
|
|
|
|
.ToList();
|
2025-10-29 19:35:04 +01:00
|
|
|
|
|
|
|
|
if (deletableContacts.Count == 0)
|
|
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_WarningTitle,
|
|
|
|
|
Translator.ContactInfoBar_CannotDeleteRoot,
|
|
|
|
|
InfoBarMessageType.Warning);
|
2025-10-29 19:35:04 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
|
|
|
|
string.Format(Translator.ContactConfirmDialog_DeleteMultipleMessage, deletableContacts.Count),
|
|
|
|
|
Translator.ContactConfirmDialog_DeleteTitle,
|
|
|
|
|
Translator.ContactConfirmDialog_DeleteButton);
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (confirmed)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
|
|
|
|
await DeleteContactsInternalAsync(deletableContacts);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task DeleteContactsInternalAsync(IEnumerable<AccountContact> contactsToDelete)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
var addresses = contactsToDelete
|
|
|
|
|
.Select(c => c.Address)
|
|
|
|
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
|
|
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
.ToList();
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (addresses.Count == 0) return;
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await _contactService.DeleteContactsAsync(addresses);
|
|
|
|
|
await ReloadContactsAsync();
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_SuccessTitle,
|
|
|
|
|
Translator.ContactInfoBar_ContactsDeleted,
|
|
|
|
|
InfoBarMessageType.Success);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
|
|
|
string.Format(Translator.ContactInfoBar_FailedToDeleteContacts, ex.Message),
|
|
|
|
|
InfoBarMessageType.Error);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task ToggleSelection()
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
IsSelectionMode = !IsSelectionMode;
|
|
|
|
|
|
|
|
|
|
if (!IsSelectionMode)
|
|
|
|
|
{
|
|
|
|
|
SelectedContacts.Clear();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task SelectAllContacts()
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
SelectedContacts.Clear();
|
|
|
|
|
|
|
|
|
|
foreach (var contact in Contacts)
|
|
|
|
|
{
|
|
|
|
|
SelectedContacts.Add(contact);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task ClearSelection()
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await ExecuteUIThread(() => { SelectedContacts.Clear(); });
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-03-06 12:31:37 +01:00
|
|
|
private async Task PickContactPhotoAsync(AccountContactViewModel contactViewModel)
|
2025-10-29 19:35:04 +01:00
|
|
|
{
|
2026-03-06 12:31:37 +01:00
|
|
|
var contact = contactViewModel?.SourceContact;
|
2025-10-29 19:35:04 +01:00
|
|
|
if (contact == null) return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var files = await _dialogService.PickFilesAsync(".png", ".jpg", ".jpeg");
|
|
|
|
|
|
|
|
|
|
if (files?.Any() == true)
|
|
|
|
|
{
|
|
|
|
|
var file = files.First();
|
|
|
|
|
|
2026-03-07 11:43:56 +01:00
|
|
|
if (contact.ContactPictureFileId.HasValue)
|
|
|
|
|
await _contactPictureFileService.DeleteContactPictureAsync(contact.ContactPictureFileId.Value);
|
|
|
|
|
|
|
|
|
|
contact.ContactPictureFileId = await _contactPictureFileService
|
|
|
|
|
.SaveContactPictureAsync(file.Data)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
2025-10-29 19:35:04 +01:00
|
|
|
await _contactService.UpdateContactAsync(contact);
|
2026-02-09 22:39:30 +01:00
|
|
|
await RefreshContactInUiAsync(contact);
|
2025-10-29 19:35:04 +01:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_SuccessTitle,
|
|
|
|
|
Translator.ContactInfoBar_ContactPhotoUpdated,
|
|
|
|
|
InfoBarMessageType.Success);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.ContactInfoBar_ErrorTitle,
|
|
|
|
|
string.Format(Translator.ContactInfoBar_FailedToUpdatePhoto, ex.Message),
|
|
|
|
|
InfoBarMessageType.Error);
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task RefreshContactInUiAsync(AccountContact contact)
|
|
|
|
|
{
|
|
|
|
|
if (contact == null || string.IsNullOrWhiteSpace(contact.Address))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
ReplaceContactByAddress(Contacts, contact);
|
|
|
|
|
ReplaceContactByAddress(SelectedContacts, contact);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 12:31:37 +01:00
|
|
|
private static void ReplaceContactByAddress(ObservableCollection<AccountContactViewModel> source, AccountContact updatedContact)
|
2026-02-09 22:39:30 +01:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-06 12:31:37 +01:00
|
|
|
source[index] = new AccountContactViewModel(CloneContact(updatedContact));
|
2026-02-09 22:39:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static AccountContact CloneContact(AccountContact contact)
|
|
|
|
|
=> new()
|
|
|
|
|
{
|
|
|
|
|
Address = contact.Address,
|
|
|
|
|
Name = contact.Name,
|
2026-03-07 11:43:56 +01:00
|
|
|
ContactPictureFileId = contact.ContactPictureFileId,
|
2026-02-09 22:39:30 +01:00
|
|
|
IsRootContact = contact.IsRootContact,
|
|
|
|
|
IsOverridden = contact.IsOverridden
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-29 19:35:04 +01:00
|
|
|
partial void OnSearchQueryChanged(string value)
|
|
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
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.
|
|
|
|
|
}
|
2025-10-29 19:35:04 +01:00
|
|
|
}
|
|
|
|
|
}
|