Files
Wino-Mail/Wino.Mail.ViewModels/MailAppShellViewModel.cs
T

1486 lines
55 KiB
C#
Raw Normal View History

2026-03-08 11:22:41 +01:00
using System;
2024-04-18 01:44:37 +02:00
using System.Collections.Generic;
using System.Diagnostics;
2024-04-18 01:44:37 +02:00
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using MoreLinq.Extensions;
using Serilog;
using Wino.Core.Domain;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.MenuItems;
2026-03-08 13:21:42 +01:00
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Launch;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
2026-04-16 13:46:52 +02:00
using Wino.Core.Requests.Folder;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Client.Shell;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
namespace Wino.Mail.ViewModels;
2025-12-26 20:46:48 +01:00
public partial class MailAppShellViewModel : MailBaseViewModel,
2026-03-11 01:39:32 +01:00
IMailShellClient,
2025-02-16 11:54:23 +01:00
IRecipient<MailtoProtocolMessageRequested>,
IRecipient<RefreshUnreadCountsMessage>,
IRecipient<AccountsMenuRefreshRequested>,
IRecipient<MergedInboxRenamed>,
IRecipient<LanguageChanged>,
IRecipient<AccountMenuItemsReordered>,
IRecipient<AccountSynchronizationProgressUpdatedMessage>,
2025-02-16 11:54:23 +01:00
IRecipient<NavigateAppPreferencesRequested>,
2025-10-25 10:22:35 +02:00
IRecipient<AccountFolderConfigurationUpdated>,
IRecipient<AccountRemovedMessage>,
2026-03-14 14:14:58 +01:00
IRecipient<AccountUpdatedMessage>
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
#region Menu Items
[ObservableProperty]
private object selectedMenuItem;
private IAccountMenuItem latestSelectedAccountMenuItem;
public MenuItemCollection FooterItems { get; set; }
public MenuItemCollection MenuItems { get; set; }
private readonly SettingsItem SettingsItem = new SettingsItem();
2025-10-29 19:35:04 +01:00
private readonly ContactsMenuItem ContactsMenuItem = new ContactsMenuItem();
2026-03-08 11:22:41 +01:00
private readonly StoreUpdateMenuItem StoreUpdateMenuItem = new StoreUpdateMenuItem();
2025-02-16 11:54:23 +01:00
public IMenuItem CreateMailMenuItem = new NewMailMenuItem();
#endregion
private const string IsActivateStartupLaunchAskedKey = nameof(IsActivateStartupLaunchAskedKey);
public IStatePersistanceService StatePersistenceService { get; }
public IPreferencesService PreferencesService { get; }
public INavigationService NavigationService { get; }
2026-03-11 01:39:32 +01:00
public WinoApplicationMode Mode => WinoApplicationMode.Mail;
public bool HandlesNavigationSelection => true;
public IMenuItem CreatePrimaryMenuItem => CreateMailMenuItem;
2025-02-16 11:54:23 +01:00
private readonly IFolderService _folderService;
2026-04-15 01:18:07 +02:00
private readonly IMailCategoryService _mailCategoryService;
2025-02-16 11:54:23 +01:00
private readonly IConfigurationService _configurationService;
private readonly IStartupBehaviorService _startupBehaviorService;
private readonly IAccountService _accountService;
private readonly IContextMenuItemService _contextMenuItemService;
private readonly IStoreRatingService _storeRatingService;
private readonly ILaunchProtocolService _launchProtocolService;
private readonly INotificationBuilder _notificationBuilder;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IMailDialogService _dialogService;
private readonly IMimeFileService _mimeFileService;
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
2026-03-08 11:22:41 +01:00
private readonly IStoreUpdateService _storeUpdateService;
private readonly IShareActivationService _shareActivationService;
2025-02-16 11:54:23 +01:00
private readonly INativeAppService _nativeAppService;
private readonly IMailService _mailService;
2026-03-18 09:00:26 +01:00
private bool _hasRegisteredPersistentRecipients;
private readonly SemaphoreSlim _menuRefreshSemaphore = new(1, 1);
2025-02-16 11:54:23 +01:00
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
2025-12-26 20:46:48 +01:00
public MailAppShellViewModel(IMailDialogService dialogService,
2025-02-16 11:54:23 +01:00
INavigationService navigationService,
IMimeFileService mimeFileService,
INativeAppService nativeAppService,
IMailService mailService,
2026-04-15 01:18:07 +02:00
IMailCategoryService mailCategoryService,
2025-02-16 11:54:23 +01:00
IAccountService accountService,
IContextMenuItemService contextMenuItemService,
IStoreRatingService storeRatingService,
IPreferencesService preferencesService,
ILaunchProtocolService launchProtocolService,
INotificationBuilder notificationBuilder,
IWinoRequestDelegator winoRequestDelegator,
IFolderService folderService,
IStatePersistanceService statePersistanceService,
IConfigurationService configurationService,
IStartupBehaviorService startupBehaviorService,
2026-03-02 00:44:29 +01:00
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
IStoreUpdateService storeUpdateService,
IShareActivationService shareActivationService)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
StatePersistenceService = statePersistanceService;
PreferencesService = preferencesService;
_dialogService = dialogService;
NavigationService = navigationService;
_configurationService = configurationService;
_startupBehaviorService = startupBehaviorService;
_mimeFileService = mimeFileService;
_nativeAppService = nativeAppService;
_mailService = mailService;
2026-04-15 01:18:07 +02:00
_mailCategoryService = mailCategoryService;
2025-02-16 11:54:23 +01:00
_folderService = folderService;
_accountService = accountService;
_contextMenuItemService = contextMenuItemService;
_storeRatingService = storeRatingService;
_launchProtocolService = launchProtocolService;
_notificationBuilder = notificationBuilder;
_winoRequestDelegator = winoRequestDelegator;
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
2026-03-08 11:22:41 +01:00
_storeUpdateService = storeUpdateService;
_shareActivationService = shareActivationService;
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
protected override void OnDispatcherAssigned()
{
base.OnDispatcherAssigned();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
MenuItems = new MenuItemCollection(Dispatcher);
FooterItems = new MenuItemCollection(Dispatcher);
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folder)
{
if (folder == null || folder.SpecialFolderType == SpecialFolderType.Category || folder.SpecialFolderType == SpecialFolderType.More)
return default;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
return _contextMenuItemService.GetFolderContextMenuActions(folder);
}
2024-04-18 01:44:37 +02:00
2026-03-08 11:22:41 +01:00
private async Task CreateFooterItemsAsync(bool showNotification = false)
2025-02-16 11:54:23 +01:00
{
await ExecuteUIThread(() =>
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
FooterItems.Clear();
});
}
2025-02-16 11:35:43 +01:00
private static void ApplySynchronizationProgress(IAccountMenuItem accountMenuItem, SynchronizationProgressCategory category)
{
AccountSynchronizationProgress progress;
try
{
progress = SynchronizationManager.Instance.GetSynchronizationProgress(
accountMenuItem.HoldingAccounts.First().Id,
category);
}
catch (InvalidOperationException)
{
return;
}
accountMenuItem.ApplySynchronizationProgress(progress);
}
2025-02-16 11:54:23 +01:00
private async Task LoadAccountsAsync()
{
// First clear all account menu items.
MenuItems.RemoveRange(MenuItems.Where(a => a is IAccountMenuItem));
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
List<Guid> initializedAccountIds = new();
foreach (var account in accounts)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
// Already initialized with one of the previous merged accounts.
2025-02-16 11:54:23 +01:00
if (initializedAccountIds.Contains(account.Id)) continue;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
bool isMergedAccount = account.MergedInboxId != null;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (isMergedAccount)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var mergedAccountId = account.MergedInboxId.Value;
var mergedAccounts = accounts.Where(a => a.MergedInboxId == mergedAccountId);
var mergedInbox = mergedAccounts.First().MergedInbox;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var mergedAccountMenuItem = new MergedAccountMenuItem(mergedInbox, mergedAccounts, null);
2025-02-16 11:54:23 +01:00
foreach (var mergedAccount in mergedAccounts)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
initializedAccountIds.Add(mergedAccount.Id);
var accountMenuItem = new AccountMenuItem(mergedAccount, mergedAccountMenuItem);
ApplySynchronizationProgress(accountMenuItem, SynchronizationProgressCategory.Mail);
mergedAccountMenuItem.SubMenuItems.Add(accountMenuItem);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
mergedAccountMenuItem.RefreshSynchronizationProgress();
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() =>
{
2025-02-16 11:54:23 +01:00
MenuItems.Add(mergedAccountMenuItem);
});
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
else
2024-04-18 01:44:37 +02:00
{
var accountMenuItem = new AccountMenuItem(account, null);
ApplySynchronizationProgress(accountMenuItem, SynchronizationProgressCategory.Mail);
2024-04-18 01:44:37 +02:00
await ExecuteUIThread(() =>
{
MenuItems.Add(accountMenuItem);
2024-04-18 01:44:37 +02:00
});
2025-02-16 11:54:23 +01:00
initializedAccountIds.Add(account.Id);
2024-04-18 01:44:37 +02:00
}
}
2025-02-16 11:54:23 +01:00
// Re-assign latest selected account menu item for containers to reflect changes better.
// Also , this will ensure that the latest selected account is still selected after re-creation.
if (latestSelectedAccountMenuItem != null && MenuItems.TryGetAccountMenuItem(latestSelectedAccountMenuItem.EntityId.GetValueOrDefault(), out IAccountMenuItem foundLatestSelectedAccountMenuItem))
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() =>
{
foundLatestSelectedAccountMenuItem.IsSelected = true;
});
}
}
2026-03-08 11:22:41 +01:00
private async void PreferencesServiceChanged(object sender, string e)
{
if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
{
await CreateFooterItemsAsync();
}
}
2025-02-16 11:54:23 +01:00
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
2026-03-18 09:00:26 +01:00
if (!_hasRegisteredPersistentRecipients)
{
RegisterRecipients();
_hasRegisteredPersistentRecipients = true;
}
2025-12-27 19:16:24 +01:00
2026-03-11 01:39:32 +01:00
var activationContext = parameters as ShellModeActivationContext;
var shouldRunStartupFlows = (activationContext?.IsInitialActivation ?? true) &&
activationContext?.SuppressStartupFlows != true;
var hasExistingAccountMenuItems = MenuItems?.OfType<IAccountMenuItem>().Any() == true;
2026-03-11 01:39:32 +01:00
2026-03-08 11:22:41 +01:00
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
2026-03-11 19:26:37 +01:00
await CreateFooterItemsAsync(true);
if (!hasExistingAccountMenuItems)
2026-03-11 19:26:37 +01:00
{
await RecreateMenuItemsAsync();
}
2025-12-27 19:16:24 +01:00
2025-02-16 11:54:23 +01:00
await ProcessLaunchOptionsAsync();
await HandlePendingShareRequestAsync();
await ValidateWebView2RuntimeAsync();
2024-04-18 01:44:37 +02:00
2026-03-11 01:39:32 +01:00
if (shouldRunStartupFlows && !Debugger.IsAttached)
{
await ForceAllAccountSynchronizationsAsync();
}
2026-03-11 01:39:32 +01:00
if (shouldRunStartupFlows)
{
await MakeSureEnableStartupLaunchAsync();
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
private async Task ValidateWebView2RuntimeAsync()
{
var isRuntimeAvailable = await _webView2RuntimeValidatorService.IsRuntimeAvailableAsync();
if (!isRuntimeAvailable)
{
await ExecuteUIThread(() => _notificationBuilder.CreateWebView2RuntimeMissingNotification());
}
}
2026-03-08 11:22:41 +01:00
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
}
public void PrepareForShellShutdown()
{
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
if (_hasRegisteredPersistentRecipients)
{
UnregisterRecipients();
_hasRegisteredPersistentRecipients = false;
}
latestSelectedAccountMenuItem = null;
SelectedMenuItem = null;
MenuItems?.Clear();
MenuItems?.Add(CreateMailMenuItem);
FooterItems?.Clear();
}
2025-02-16 11:54:23 +01:00
private async Task MakeSureEnableStartupLaunchAsync()
{
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
2024-08-22 00:51:10 +02:00
{
2025-02-16 11:54:23 +01:00
var currentBehavior = await _startupBehaviorService.GetCurrentStartupBehaviorAsync();
2024-08-22 00:51:10 +02:00
2025-02-16 11:54:23 +01:00
// User somehow already enabled Wino before the first launch.
if (currentBehavior == StartupBehaviorResult.Enabled)
{
_configurationService.Set(IsActivateStartupLaunchAskedKey, true);
return;
}
2024-08-22 00:51:10 +02:00
2025-02-16 11:54:23 +01:00
bool isAccepted = await _dialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_EnableStartupLaunchTitle,
Translator.DialogMessage_EnableStartupLaunchMessage,
Translator.Buttons_Yes,
WinoCustomMessageDialogIcon.Information,
Translator.Buttons_No);
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
bool shouldDisplayLaterOnMessage = !isAccepted;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (isAccepted)
{
var behavior = await _startupBehaviorService.ToggleStartupBehavior(true);
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
shouldDisplayLaterOnMessage = behavior != StartupBehaviorResult.Enabled;
2024-08-22 00:51:10 +02:00
}
2025-02-16 11:54:23 +01:00
if (shouldDisplayLaterOnMessage)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await _dialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_EnableStartupLaunchTitle,
Translator.DialogMessage_EnableStartupLaunchDeniedMessage,
Translator.Buttons_Close,
WinoCustomMessageDialogIcon.Information);
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
_configurationService.Set(IsActivateStartupLaunchAskedKey, true);
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task ForceAllAccountSynchronizationsAsync()
{
// Run Inbox synchronization for all accounts on startup.
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var options = new MailSynchronizationOptions()
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
AccountId = account.Id,
Type = MailSynchronizationType.FullFolders
};
2024-04-18 01:44:37 +02:00
2025-10-12 16:23:33 +02:00
Messenger.Send(new NewMailSynchronizationRequested(options));
2025-02-16 11:54:23 +01:00
}
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Navigate to startup account's Inbox.
private async Task ProcessLaunchOptionsAsync()
{
try
{
// Check whether we have saved navigation item from toast.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
bool hasToastActivation = _launchProtocolService.LaunchParameter != null;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (hasToastActivation)
{
if (_launchProtocolService.LaunchParameter is AccountMenuItemExtended accountExtendedMessage)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
// Find the account that this folder and mail belongs to.
var account = await _mailService.GetMailAccountByUniqueIdAsync(accountExtendedMessage.NavigateMailItem.UniqueId).ConfigureAwait(false);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (account != null && MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem))
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await ChangeLoadedAccountAsync(accountMenuItem);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(accountExtendedMessage);
_launchProtocolService.LaunchParameter = null;
2024-04-18 01:44:37 +02:00
}
else
{
await ProcessLaunchDefaultAsync();
2024-04-18 01:44:37 +02:00
}
}
}
2025-02-16 11:43:30 +01:00
else
{
2025-02-16 11:54:23 +01:00
bool hasMailtoActivation = _launchProtocolService.MailToUri != null;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (hasMailtoActivation)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
// mailto activation. Create new mail with specific delivered address as receiver.
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger.Default.Send(new MailtoProtocolMessageRequested());
2025-02-16 11:43:30 +01:00
}
else
{
2025-02-16 11:54:23 +01:00
// Use default startup extending.
await ProcessLaunchDefaultAsync();
2024-04-18 01:44:37 +02:00
}
}
}
2025-02-16 11:54:23 +01:00
catch (Exception ex)
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
Log.Error(ex, "Failed to process launch options.");
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task ProcessLaunchDefaultAsync()
{
if (PreferencesService.StartupEntityId == null)
2025-02-16 11:35:43 +01:00
{
NavigateToWelcomeWizard();
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
else
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
var startupEntityId = PreferencesService.StartupEntityId.Value;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// startupEntityId is the id of the entity to be expanded on startup.
// This can be either AccountId or MergedAccountId right now.
// If accountId, we'll find the root account and extend Inbox folder for it.
// If mergedAccountId, merged account's Inbox folder will be extended.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var startupEntityMenuItem = MenuItems.FirstOrDefault(a => a.EntityId == startupEntityId);
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (startupEntityMenuItem != null)
{
startupEntityMenuItem.Expand();
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if (startupEntityMenuItem is IAccountMenuItem startupAccountMenuItem)
{
2025-02-16 11:54:23 +01:00
await ChangeLoadedAccountAsync(startupAccountMenuItem);
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
else
2025-02-16 11:43:30 +01:00
{
// Fallback to the welcome wizard if startup entity is not found.
NavigateToWelcomeWizard();
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
public async Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource<bool> folderInitAwaitTask = null)
{
// It's already there. Don't navigate again.
if (SelectedMenuItem == baseFolderMenuItem) return;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() =>
{
SelectedMenuItem = baseFolderMenuItem;
baseFolderMenuItem.IsSelected = true;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
folderInitAwaitTask ??= new TaskCompletionSource<bool>();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, folderInitAwaitTask);
2024-04-18 01:44:37 +02:00
2025-12-26 20:46:48 +01:00
NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.InnerShellFrame);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
UpdateWindowTitleForFolder(baseFolderMenuItem);
});
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// Wait until mail list page picks up the event and finish initialization of the mails.
await folderInitAwaitTask.Task;
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
private void UpdateWindowTitleForFolder(IBaseFolderMenuItem folder)
{
StatePersistenceService.CoreWindowTitle = $"{folder.AssignedAccountName} - {folder.FolderName}";
}
2025-02-16 11:43:30 +01:00
private void UpdateWindowTitle(string title)
{
StatePersistenceService.CoreWindowTitle = title;
}
2025-02-16 11:54:23 +01:00
private async Task NavigateSpecialFolderAsync(MailAccount account, SpecialFolderType specialFolderType, bool extendAccountMenu)
{
try
{
if (account == null) return;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if (!MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem)) return;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// First make sure to navigate to the given accounnt.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (latestSelectedAccountMenuItem != accountMenuItem)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
await ChangeLoadedAccountAsync(accountMenuItem, false);
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Account folders are already initialized.
// Try to find the special folder menu item and navigate to it.
if (latestSelectedAccountMenuItem is IMergedAccountMenuItem latestMergedAccountMenuItem)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
if (MenuItems.TryGetMergedAccountSpecialFolderMenuItem(latestSelectedAccountMenuItem.EntityId.Value, specialFolderType, out IBaseFolderMenuItem mergedFolderMenuItem))
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
await NavigateFolderAsync(mergedFolderMenuItem);
2025-02-16 11:43:30 +01:00
}
}
2025-02-16 11:54:23 +01:00
else if (latestSelectedAccountMenuItem is IAccountMenuItem latestAccountMenuItem)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
if (MenuItems.TryGetSpecialFolderMenuItem(account.Id, specialFolderType, out FolderMenuItem rootFolderMenuItem))
{
await NavigateFolderAsync(rootFolderMenuItem);
}
2024-04-18 01:44:37 +02:00
}
}
2025-02-16 11:54:23 +01:00
catch (Exception ex)
{
Log.Error(ex, "Failed to navigate to Inbox.");
}
}
/// <summary>
/// Performs move operation for given items to target folder.
/// Used with drag and drop from Shell.
/// </summary>
/// <param name="items">Items to move.</param>
/// <param name="targetFolderMenuItem">Folder menu item to move to. Can be merged folder as well.</param>
public async Task PerformMoveOperationAsync(IEnumerable<MailCopy> items, IBaseFolderMenuItem targetFolderMenuItem)
{
if (!items.Any() || targetFolderMenuItem == null) return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// User dropped mails to merged account folder.
if (targetFolderMenuItem is IMergedAccountFolderMenuItem mergedAccountFolderMenuItem)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
// Mail items must be grouped by their account and move
// operation should be targeted towards that account's special type.
// Multiple move packages will be created if there are multiple accounts.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var folderSpecialType = mergedAccountFolderMenuItem.SpecialFolderType;
2025-02-16 11:54:23 +01:00
var groupedByAccount = items.GroupBy(a => a.AssignedAccount.Id);
2025-02-16 11:54:23 +01:00
foreach (var group in groupedByAccount)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
var accountId = group.Key;
// Find the target folder for this account.
var handlingAccountFolder = mergedAccountFolderMenuItem.HandlingFolders.FirstOrDefault(a => a.MailAccountId == accountId);
if (handlingAccountFolder == null)
{
Log.Warning("Failed to find the account in the merged account folder menu item for account id {AccountId}", accountId);
continue;
}
var package = new MailOperationPreperationRequest(MailOperation.Move, group, false, handlingAccountFolder);
await _winoRequestDelegator.ExecuteAsync(package);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
else if (targetFolderMenuItem is IFolderMenuItem singleFolderMenuItem)
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
// User dropped mails to a single folder.
// Create a single move package for this folder.
var package = new MailOperationPreperationRequest(MailOperation.Move, items, false, targetFolderMenuItem.HandlingFolders.First());
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
await _winoRequestDelegator.ExecuteAsync(package);
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public async Task PerformFolderOperationAsync(FolderOperation operation, IBaseFolderMenuItem folderMenuItem)
{
if (folderMenuItem == null)
return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Ask confirmation for cleaning up the folder.
if (operation == FolderOperation.Empty)
{
var result = await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_CleanupFolderMessage, Translator.DialogMessage_CleanupFolderTitle, Translator.Buttons_Yes);
2024-06-07 23:58:51 +02:00
2025-02-16 11:54:23 +01:00
if (!result) return;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach (var folder in folderMenuItem.HandlingFolders)
{
if (folder is MailItemFolder realFolder)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
var folderPrepRequest = new FolderOperationPreperationRequest(operation, realFolder);
await _winoRequestDelegator.ExecuteAsync(folderPrepRequest);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Refresh the pins.
if (operation == FolderOperation.Pin || operation == FolderOperation.Unpin)
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
Messenger.Send(new AccountsMenuRefreshRequested(true));
}
}
2025-02-16 11:35:43 +01:00
2026-04-16 13:46:52 +02:00
public async Task CreateRootFolderAsync(IAccountMenuItem accountMenuItem)
{
var account = accountMenuItem?.HoldingAccounts?.FirstOrDefault();
if (account == null)
return;
var folderName = await _dialogService.ShowTextInputDialogAsync(
string.Empty,
Translator.AccountContextMenu_CreateFolder,
Translator.DialogMessage_CreateFolderMessage,
Translator.Buttons_Create);
if (string.IsNullOrWhiteSpace(folderName))
return;
var placeholderFolder = new MailItemFolder
{
MailAccountId = account.Id
};
await _winoRequestDelegator.ExecuteAsync(account.Id, [new CreateRootFolderRequest(placeholderFolder, folderName.Trim())]);
}
public Task HandleAccountAttentionAsync(MailAccount account)
=> FixAccountIssuesAsync(account);
private void TriggerFullSynchronization(MailAccount account)
2025-02-16 11:54:23 +01:00
{
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
{
AccountId = account.Id,
Type = MailSynchronizationType.FullFolders
}));
2025-02-16 11:35:43 +01:00
if (account.IsCalendarAccessGranted)
{
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
{
AccountId = account.Id,
Type = CalendarSynchronizationType.CalendarEvents
}));
}
}
private async Task FixAccountIssuesAsync(MailAccount account)
{
2025-02-16 11:54:23 +01:00
try
{
if (account.AttentionReason == AccountAttentionReason.InvalidCredentials)
{
if (account.ProviderType is MailProviderType.Gmail or MailProviderType.Outlook)
{
await SynchronizationManager.Instance.HandleAuthorizationAsync(
account.ProviderType,
account,
account.ProviderType == MailProviderType.Gmail);
await _accountService.ClearAccountAttentionAsync(account.Id);
_dialogService.InfoBarMessage(
Translator.Info_AccountIssueFixSuccessTitle,
Translator.Info_AccountIssueFixSuccessMessage,
InfoBarMessageType.Success);
TriggerFullSynchronization(account);
return;
}
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage);
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.ImapCalDavSettingsPage_TitleEdit,
WinoPage.ImapCalDavSettingsPage,
ImapCalDavSettingsNavigationContext.CreateForEditMode(account.Id)));
_dialogService.InfoBarMessage(
Translator.Info_AccountIssueFixSuccessTitle,
Translator.Info_AccountIssueFixImapMessage,
InfoBarMessageType.Information);
return;
}
2025-02-16 11:54:23 +01:00
else if (account.AttentionReason == AccountAttentionReason.MissingSystemFolderConfiguration)
{
2025-02-16 11:54:23 +01:00
await _dialogService.HandleSystemFolderConfigurationDialogAsync(account.Id, _folderService);
await _accountService.ClearAccountAttentionAsync(account.Id);
2025-02-16 11:35:43 +01:00
_dialogService.InfoBarMessage(
Translator.Info_AccountIssueFixSuccessTitle,
Translator.Info_AccountIssueFixSuccessMessage,
InfoBarMessageType.Success);
2025-02-16 11:35:43 +01:00
TriggerFullSynchronization(account);
}
2025-02-16 11:54:23 +01:00
}
catch (Exception ex)
{
_dialogService.InfoBarMessage(Translator.Info_AccountIssueFixFailedTitle, ex.Message, InfoBarMessageType.Error);
}
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public void NavigatePage(WinoPage winoPage)
{
NavigationService.Navigate(winoPage);
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
StatePersistenceService.CoreWindowTitle = "Wino Mail";
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
public async Task MenuItemInvokedOrSelectedAsync(IMenuItem clickedMenuItem, object parameter = null)
{
if (clickedMenuItem == null) return;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// Regular menu item clicked without page navigation.
if (clickedMenuItem is FixAccountIssuesMenuItem fixAccountItem)
{
await FixAccountIssuesAsync(fixAccountItem.Account);
}
else if (clickedMenuItem is RateMenuItem)
{
await _storeRatingService.LaunchStorePageForReviewAsync();
}
else if (clickedMenuItem is NewMailMenuItem)
{
await HandleCreateNewMailAsync();
}
2026-04-15 01:18:07 +02:00
else if (clickedMenuItem is IBaseFolderMenuItem baseFolderMenuItem &&
(clickedMenuItem is IMailCategoryMenuItem or IMergedMailCategoryMenuItem || baseFolderMenuItem.HandlingFolders.All(a => a.IsMoveTarget)))
2025-02-16 11:54:23 +01:00
{
// Don't navigate to base folders that contain non-move target folders.
// Theory: This is a special folder like Categories or More. Don't navigate to it.
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// Prompt user rating dialog if eligible.
_ = _storeRatingService.PromptRatingDialogAsync();
2025-02-16 11:54:23 +01:00
await NavigateFolderAsync(baseFolderMenuItem);
}
else if (clickedMenuItem is MergedAccountMenuItem clickedMergedAccountMenuItem && latestSelectedAccountMenuItem != clickedMenuItem)
{
// Don't navigate to merged account if it's already selected. Preserve user's already selected folder.
await ChangeLoadedAccountAsync(clickedMergedAccountMenuItem, true);
}
else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem)
2025-02-16 11:54:23 +01:00
{
// Changing loaded account.
if (latestSelectedAccountMenuItem != clickedAccountMenuItem)
{
await ChangeLoadedAccountAsync(clickedAccountMenuItem);
}
else
{
// Clicked on the same account. Just navigate to Inbox.
await NavigateInboxAsync(clickedAccountMenuItem);
}
2025-02-16 11:54:23 +01:00
}
}
public async Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true)
{
if (clickedBaseAccountMenuItem == null) return;
// User clicked an account in Windows Mail style menu.
// List folders for this account and select Inbox.
await MenuItems.SetAccountMenuItemEnabledStatusAsync(false);
// Load account folder structure and replace the visible folders.
var folders = await _folderService.GetAccountFoldersForDisplayAsync(clickedBaseAccountMenuItem);
await ExecuteUIThread(() =>
{
clickedBaseAccountMenuItem.IsEnabled = false;
if (latestSelectedAccountMenuItem != null)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
latestSelectedAccountMenuItem.IsSelected = false;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
clickedBaseAccountMenuItem.IsSelected = true;
latestSelectedAccountMenuItem = clickedBaseAccountMenuItem;
});
await MenuItems.ReplaceFoldersAsync(folders);
await UpdateUnreadItemCountAsync();
await MenuItems.SetAccountMenuItemEnabledStatusAsync(true);
if (navigateInbox)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
await Task.Yield();
await NavigateInboxAsync(clickedBaseAccountMenuItem);
}
}
2025-02-16 11:54:23 +01:00
private async Task UpdateUnreadItemCountAsync()
{
// Get visible account menu items, ordered by merged accounts at the last.
// We will update the unread counts for all single accounts and trigger UI refresh for merged menu items.
2026-04-15 01:18:07 +02:00
List<IAccountMenuItem> accountMenuItems = null;
await ExecuteUIThread(() =>
{
accountMenuItems = MenuItems
.GetAllAccountMenuItems()
.OrderBy(a => a.HoldingAccounts.Count())
.ToList();
});
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// Individually get all single accounts' unread counts.
2026-04-15 01:18:07 +02:00
var accountIds = accountMenuItems.OfType<AccountMenuItem>().Select(a => a.AccountId).ToList();
2025-02-16 11:54:23 +01:00
var unreadCountResult = await _folderService.GetUnreadItemCountResultsAsync(accountIds).ConfigureAwait(false);
2026-04-15 01:18:07 +02:00
var unreadCategoryCountResult = await _mailCategoryService.GetUnreadCategoryCountResultsAsync(accountIds).ConfigureAwait(false);
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// Recursively update all folders' unread counts to 0.
// Query above only returns unread counts that exists. We need to reset the rest to 0 first.
await ExecuteUIThread(() =>
{
MenuItems.UpdateUnreadItemCountsToZero();
});
foreach (var accountMenuItem in accountMenuItems)
{
if (accountMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() =>
{
mergedAccountMenuItem.RefreshFolderItemCount();
});
}
else
{
await ExecuteUIThread(() =>
{
accountMenuItem.UnreadItemCount = unreadCountResult
.Where(a => a.AccountId == accountMenuItem.HoldingAccounts.First().Id && a.SpecialFolderType == SpecialFolderType.Inbox)
.Sum(a => a.UnreadItemCount);
});
}
}
2025-02-16 11:54:23 +01:00
// Try to update unread counts for all folders.
foreach (var unreadCount in unreadCountResult)
{
if (MenuItems.TryGetFolderMenuItem(unreadCount.FolderId, out IBaseFolderMenuItem folderMenuItem))
{
2025-02-16 11:54:23 +01:00
if (folderMenuItem is IMergedAccountFolderMenuItem mergedAccountFolderMenuItem)
{
await ExecuteUIThread(() =>
{
2025-02-16 11:54:23 +01:00
folderMenuItem.UnreadItemCount = unreadCountResult.Where(a => a.SpecialFolderType == unreadCount.SpecialFolderType && mergedAccountFolderMenuItem.HandlingFolders.Select(b => b.Id).Contains(a.FolderId)).Sum(a => a.UnreadItemCount);
});
}
else
{
await ExecuteUIThread(() =>
{
2025-02-16 11:54:23 +01:00
folderMenuItem.UnreadItemCount = unreadCount.UnreadItemCount;
});
}
}
2025-02-16 11:54:23 +01:00
}
2026-04-15 01:18:07 +02:00
foreach (var unreadCategoryCount in unreadCategoryCountResult)
{
if (MenuItems.TryGetCategoryMenuItem(unreadCategoryCount.CategoryId, out var categoryMenuItem))
{
if (categoryMenuItem is IMergedMailCategoryMenuItem mergedCategoryMenuItem)
{
await ExecuteUIThread(() =>
{
categoryMenuItem.UnreadItemCount = unreadCategoryCountResult
.Where(a => mergedCategoryMenuItem.Categories.Any(b => b.Id == a.CategoryId))
.Sum(a => a.UnreadItemCount);
});
}
else
{
await ExecuteUIThread(() =>
{
categoryMenuItem.UnreadItemCount = unreadCategoryCount.UnreadItemCount;
});
}
}
}
2025-02-16 11:54:23 +01:00
// Update unread badge after all unread counts are updated.
await _notificationBuilder.UpdateTaskbarIconBadgeAsync();
}
2025-02-16 11:54:23 +01:00
private async Task NavigateInboxAsync(IAccountMenuItem clickedBaseAccountMenuItem)
{
var folderInitAwaitTask = new TaskCompletionSource<bool>();
2025-02-16 11:54:23 +01:00
if (clickedBaseAccountMenuItem is AccountMenuItem accountMenuItem)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
if (MenuItems.TryGetWindowsStyleRootSpecialFolderMenuItem(accountMenuItem.AccountId, SpecialFolderType.Inbox, out FolderMenuItem inboxFolder))
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await NavigateFolderAsync(inboxFolder, folderInitAwaitTask);
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
else if (clickedBaseAccountMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
{
if (MenuItems.TryGetMergedAccountSpecialFolderMenuItem(mergedAccountMenuItem.EntityId.GetValueOrDefault(), SpecialFolderType.Inbox, out IBaseFolderMenuItem inboxFolder))
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await NavigateFolderAsync(inboxFolder, folderInitAwaitTask);
2024-04-18 01:44:37 +02:00
}
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task HandleCreateNewMailAsync()
{
_ = _storeRatingService.PromptRatingDialogAsync();
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
MailAccount operationAccount = null;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Check whether we have active folder item selected for any account.
// We have selected account. New mail creation should be targeted for this account.
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if (SelectedMenuItem is FolderMenuItem selectedFolderMenuItem)
{
operationAccount = selectedFolderMenuItem.ParentAccount;
}
// We couldn't find any account so far.
// If there is only 1 account to use, use it. If not,
// send a message for flyout so user can pick from it.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (operationAccount == null)
{
// No selected account.
// List all accounts and let user pick one.
var accounts = await _accountService.GetAccountsAsync();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (!accounts.Any())
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
var isManageAccountClicked = await _dialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_NoAccountsForCreateMailTitle,
Translator.DialogMessage_NoAccountsForCreateMailMessage,
Translator.MenuManageAccounts,
WinoCustomMessageDialogIcon.Information,
string.Empty);
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if (isManageAccountClicked)
2025-02-16 11:35:43 +01:00
{
2026-03-12 14:55:07 +01:00
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage);
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
return;
}
if (accounts.Count() == 1)
operationAccount = accounts.FirstOrDefault();
else
{
if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem)
{
// There are multiple accounts and there is no selection.
// Don't list all accounts, but only accounts that belong to Merged Inbox.
2025-02-16 11:54:23 +01:00
var mergedAccounts = accounts.Where(a => a.MergedInboxId == selectedMergedAccountMenuItem.EntityId);
2025-02-16 11:54:23 +01:00
if (!mergedAccounts.Any()) return;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(mergedAccounts.ToList()));
}
else if (latestSelectedAccountMenuItem is AccountMenuItem selectedAccountMenuItem)
{
operationAccount = selectedAccountMenuItem.HoldingAccounts.ElementAt(0);
2024-04-18 01:44:37 +02:00
}
else
{
2025-02-16 11:54:23 +01:00
// User is at some other page. List all accounts.
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(accounts));
}
}
}
2024-08-17 22:55:58 +02:00
2025-02-16 11:54:23 +01:00
if (operationAccount != null)
await CreateNewMailForAsync(operationAccount);
}
2025-02-16 11:54:23 +01:00
public async Task CreateNewMailForAsync(MailAccount account)
=> await CreateNewMailForAsync(account, null);
public async Task CreateNewMailForAsync(MailAccount account, MailShareRequest shareRequest)
2025-02-16 11:54:23 +01:00
{
if (account == null) return;
2025-02-16 11:54:23 +01:00
// Find draft folder.
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Draft);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (draftFolder == null)
{
_dialogService.InfoBarMessage(Translator.Info_DraftFolderMissingTitle,
Translator.Info_DraftFolderMissingMessage,
InfoBarMessageType.Error,
Translator.SettingConfigureSpecialFolders_Button,
() =>
{
_dialogService.HandleSystemFolderConfigurationDialogAsync(account.Id, _folderService);
});
return;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
// Navigate to draft folder.
await NavigateSpecialFolderAsync(account, SpecialFolderType.Draft, true);
// Generate empty mime message.
var draftOptions = new DraftCreationOptions
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Reason = DraftCreationReason.Empty,
MailToUri = _launchProtocolService.MailToUri
};
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false);
2024-04-18 01:44:37 +02:00
if (shareRequest?.Files?.Count > 0)
{
_shareActivationService.StagePendingComposeShareRequest(draftMailCopy.UniqueId, shareRequest);
}
2025-02-16 11:54:23 +01:00
var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason);
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
}
2024-04-18 01:44:37 +02:00
2026-03-08 13:21:42 +01:00
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Mail)
return;
if (args.Action == KeyboardShortcutAction.NewMail)
{
await HandleCreateNewMailAsync();
args.Handled = true;
}
}
2024-04-18 01:44:37 +02:00
2025-02-15 12:53:32 +01:00
2025-02-16 11:54:23 +01:00
// TODO: Handle by messaging.
private async Task SetAccountAttentionAsync(Guid accountId, AccountAttentionReason reason)
{
if (!MenuItems.TryGetAccountMenuItem(accountId, out IAccountMenuItem accountMenuItem)) return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var accountModel = accountMenuItem.HoldingAccounts.First(a => a.Id == accountId);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
accountModel.AttentionReason = reason;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _accountService.UpdateAccountAsync(accountModel);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
accountMenuItem.UpdateAccount(accountModel);
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async void Receive(MailtoProtocolMessageRequested message)
{
var accounts = await _accountService.GetAccountsAsync();
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
MailAccount targetAccount = null;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (!accounts.Any())
{
await _dialogService.ShowMessageAsync(Translator.DialogMessage_NoAccountsForCreateMailMessage,
Translator.DialogMessage_NoAccountsForCreateMailTitle,
WinoCustomMessageDialogIcon.Warning);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
else if (accounts.Count == 1)
{
targetAccount = accounts[0];
}
else
{
// User must pick an account.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
targetAccount = await _dialogService.ShowAccountPickerDialogAsync(accounts);
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if (targetAccount == null) return;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await CreateNewMailForAsync(targetAccount);
}
2024-04-18 01:44:37 +02:00
public async Task HandlePendingShareRequestAsync()
{
var shareRequest = _shareActivationService.ConsumePendingShareRequest();
if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
return;
var accounts = await _accountService.GetAccountsAsync();
if (!accounts.Any())
return;
MailAccount targetAccount = null;
if (accounts.Count == 1)
{
targetAccount = accounts[0];
}
else
{
targetAccount = await _dialogService.ShowAccountPickerDialogAsync(accounts);
}
if (targetAccount == null)
return;
await CreateNewMailForAsync(targetAccount, shareRequest);
}
2025-02-16 11:54:23 +01:00
private async Task RecreateMenuItemsAsync()
{
await _menuRefreshSemaphore.WaitAsync().ConfigureAwait(false);
try
2025-02-16 11:54:23 +01:00
{
await ExecuteUIThread(() =>
{
MenuItems.Clear();
MenuItems.Add(CreateMailMenuItem);
});
2024-04-18 01:44:37 +02:00
await LoadAccountsAsync();
}
finally
{
_menuRefreshSemaphore.Release();
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
private async Task RestoreSelectedAccountAfterMenuRefreshAsync(bool automaticallyNavigateFirstItem)
2025-02-16 11:54:23 +01:00
{
IAccountMenuItem validSelectedMenuItem = null;
bool hasPreviousSelection = latestSelectedAccountMenuItem != null;
2025-02-16 11:43:30 +01:00
if (hasPreviousSelection)
2025-02-23 17:05:46 +01:00
{
var selectedEntityId = latestSelectedAccountMenuItem.EntityId.GetValueOrDefault();
if (selectedEntityId != Guid.Empty &&
MenuItems.TryGetAccountMenuItem(selectedEntityId, out IAccountMenuItem foundSelectedMenuItem))
{
validSelectedMenuItem = foundSelectedMenuItem;
}
else
{
latestSelectedAccountMenuItem = null;
}
2025-02-23 17:05:46 +01:00
}
if (validSelectedMenuItem == null)
2024-04-18 01:44:37 +02:00
{
validSelectedMenuItem = MenuItems.FirstOrDefault(a => a is IAccountMenuItem) as IAccountMenuItem;
hasPreviousSelection = false;
2024-04-18 01:44:37 +02:00
}
if (validSelectedMenuItem != null)
{
await ChangeLoadedAccountAsync(validSelectedMenuItem, hasPreviousSelection || automaticallyNavigateFirstItem);
}
else
{
await ExecuteUIThread(() => SelectedMenuItem = null);
NavigateToWelcomeWizard();
}
}
private void NavigateToWelcomeWizard()
=> NavigationService.Navigate(
WinoPage.WelcomeHostPage,
null,
NavigationReferenceFrame.ShellFrame,
NavigationTransitionType.None);
2026-03-01 16:23:28 +01:00
private bool IsAccountCurrentlyLoaded(Guid accountId)
{
return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true;
}
private async Task RefreshLoadedAccountFolderStructureAsync(Guid accountId)
{
if (!IsAccountCurrentlyLoaded(accountId) || latestSelectedAccountMenuItem == null)
return;
var selectedFolderId = (SelectedMenuItem as IBaseFolderMenuItem)?.HandlingFolders
?.FirstOrDefault(a => a.MailAccountId == accountId)?.Id;
var folders = await _folderService.GetAccountFoldersForDisplayAsync(latestSelectedAccountMenuItem);
await MenuItems.ReplaceFoldersAsync(folders);
await UpdateUnreadItemCountAsync();
if (selectedFolderId.HasValue &&
MenuItems.TryGetFolderMenuItem(selectedFolderId.Value, out IBaseFolderMenuItem selectedFolderMenuItem))
{
await NavigateFolderAsync(selectedFolderMenuItem);
}
else
{
await NavigateInboxAsync(latestSelectedAccountMenuItem);
}
}
public async void Receive(RefreshUnreadCountsMessage message)
=> await UpdateUnreadItemCountAsync();
public async void Receive(AccountsMenuRefreshRequested message)
{
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(message.AutomaticallyNavigateFirstItem);
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async void Receive(AccountFolderConfigurationUpdated message)
{
2026-03-01 16:23:28 +01:00
await RefreshLoadedAccountFolderStructureAsync(message.AccountId);
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async void Receive(MergedInboxRenamed message)
{
var mergedInboxMenuItem = MenuItems.FirstOrDefault(a => a.EntityId == message.MergedInboxId);
2025-02-16 11:54:23 +01:00
if (mergedInboxMenuItem == null) return;
2025-02-16 11:54:23 +01:00
if (mergedInboxMenuItem is MergedAccountMenuItem mergedAccountMenuItemCasted)
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await ExecuteUIThread(() => { mergedAccountMenuItemCasted.MergedAccountName = message.NewName; });
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public async void Receive(LanguageChanged message)
{
2026-03-08 11:22:41 +01:00
await CreateFooterItemsAsync(true);
2025-02-16 11:54:23 +01:00
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
private void ReorderAccountMenuItems(Dictionary<Guid, int> newAccountOrder)
{
foreach (var item in newAccountOrder)
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
if (!MenuItems.TryGetAccountMenuItem(item.Key, out IAccountMenuItem menuItem)) return;
2025-02-16 11:54:23 +01:00
// Adding +1 since first item is always reserved for CreateMailMenuItem.
MenuItems.Move(MenuItems.IndexOf(menuItem), item.Value + 1);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public void Receive(AccountMenuItemsReordered message) => ReorderAccountMenuItems(message.newOrderDictionary);
2025-02-16 11:54:23 +01:00
private async void UpdateFolderCollection(IMailItemFolder updatedMailItemFolder)
{
var menuItem = MenuItems.GetAllFolderMenuItems(updatedMailItemFolder.Id);
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if (!menuItem.Any()) return;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
foreach (var item in menuItem)
{
await ExecuteUIThread(() =>
{
2025-02-16 11:54:23 +01:00
item.UpdateFolder(updatedMailItemFolder);
});
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
protected override void OnFolderRenamed(IMailItemFolder mailItemFolder)
{
base.OnFolderRenamed(mailItemFolder);
2025-02-16 11:54:23 +01:00
UpdateFolderCollection(mailItemFolder);
}
2026-02-08 22:20:38 +01:00
protected override async void OnFolderDeleted(MailItemFolder folder)
{
base.OnFolderDeleted(folder);
bool wasSelected = SelectedMenuItem is IBaseFolderMenuItem selectedFolder &&
selectedFolder.HandlingFolders.Any(a => a.Id == folder.Id);
await ExecuteUIThread(() => MenuItems.RemoveFolderMenuItem(folder.Id));
if (wasSelected && latestSelectedAccountMenuItem != null)
{
await NavigateInboxAsync(latestSelectedAccountMenuItem);
2026-03-01 16:23:28 +01:00
return;
2026-02-08 22:20:38 +01:00
}
2026-03-01 16:23:28 +01:00
await RefreshLoadedAccountFolderStructureAsync(folder.MailAccountId);
2026-02-08 22:20:38 +01:00
}
2025-02-16 11:54:23 +01:00
protected override void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder)
{
base.OnFolderSynchronizationEnabled(mailItemFolder);
2025-02-16 11:54:23 +01:00
UpdateFolderCollection(mailItemFolder);
}
public async void Receive(AccountSynchronizationProgressUpdatedMessage message)
2025-02-16 11:54:23 +01:00
{
var progress = message.Progress;
if (progress.Category != SynchronizationProgressCategory.Mail)
return;
var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(progress.AccountId);
2025-02-16 11:54:23 +01:00
if (accountMenuItem == null) return;
2025-10-31 00:51:27 +01:00
await ExecuteUIThread(() =>
{
accountMenuItem.ApplySynchronizationProgress(progress);
2025-12-26 20:46:48 +01:00
2025-10-31 00:51:27 +01:00
// If this account is part of a merged inbox, update the merged inbox progress as well
if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
{
mergedAccountMenuItem.RefreshSynchronizationProgress();
}
});
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
public async void Receive(NavigateAppPreferencesRequested message)
{
2026-03-12 19:04:47 +01:00
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.AppPreferencesPage);
2024-04-18 01:44:37 +02:00
}
protected override void RegisterRecipients()
{
base.RegisterRecipients();
2025-10-25 10:22:35 +02:00
Messenger.Register<AccountRemovedMessage>(this);
Messenger.Register<AccountUpdatedMessage>(this);
Messenger.Register<MailtoProtocolMessageRequested>(this);
Messenger.Register<RefreshUnreadCountsMessage>(this);
Messenger.Register<AccountsMenuRefreshRequested>(this);
Messenger.Register<MergedInboxRenamed>(this);
Messenger.Register<LanguageChanged>(this);
Messenger.Register<AccountMenuItemsReordered>(this);
Messenger.Register<AccountSynchronizationProgressUpdatedMessage>(this);
Messenger.Register<NavigateAppPreferencesRequested>(this);
Messenger.Register<AccountFolderConfigurationUpdated>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
2025-10-25 10:22:35 +02:00
2025-12-26 20:46:48 +01:00
Messenger.Unregister<AccountRemovedMessage>(this);
Messenger.Unregister<AccountUpdatedMessage>(this);
Messenger.Unregister<MailtoProtocolMessageRequested>(this);
Messenger.Unregister<RefreshUnreadCountsMessage>(this);
Messenger.Unregister<AccountsMenuRefreshRequested>(this);
Messenger.Unregister<MergedInboxRenamed>(this);
Messenger.Unregister<LanguageChanged>(this);
Messenger.Unregister<AccountMenuItemsReordered>(this);
Messenger.Unregister<AccountSynchronizationProgressUpdatedMessage>(this);
Messenger.Unregister<NavigateAppPreferencesRequested>(this);
Messenger.Unregister<AccountFolderConfigurationUpdated>(this);
}
2025-10-25 10:22:35 +02:00
public async void Receive(AccountRemovedMessage message)
{
var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
if (!remainingAccounts.Any())
{
latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() =>
{
SelectedMenuItem = null;
MenuItems?.Clear();
MenuItems?.Add(CreateMailMenuItem);
});
return;
}
if (latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == message.Account.Id) == true)
{
latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() => SelectedMenuItem = null);
}
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
}
2025-10-25 10:22:35 +02:00
2026-03-14 14:14:58 +01:00
public async Task HandleAccountCreatedAsync(MailAccount createdAccount)
2025-10-25 10:22:35 +02:00
{
latestSelectedAccountMenuItem = null;
await RecreateMenuItemsAsync();
2026-03-14 14:14:58 +01:00
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem))
{
Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id);
return;
}
2025-10-25 10:22:35 +02:00
await ChangeLoadedAccountAsync(createdMenuItem);
// Each created account should start a new synchronization automatically.
var options = new MailSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.FullFolders,
};
Messenger.Send(new NewMailSynchronizationRequested(options));
if (createdAccount.IsCalendarAccessGranted)
{
var calendarOptions = new CalendarSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = CalendarSynchronizationType.CalendarEvents
};
Messenger.Send(new NewCalendarSynchronizationRequested(calendarOptions));
}
2025-10-25 10:22:35 +02:00
try
{
await _nativeAppService.PinAppToTaskbarAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to pin Wino to taskbar.");
}
}
public async void Receive(AccountUpdatedMessage message)
{
var updatedAccount = message.Account;
await ExecuteUIThread(() =>
{
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem))
{
foundAccountMenuItem.UpdateAccount(updatedAccount);
}
});
}
2026-03-11 01:39:32 +01:00
void IShellClient.Activate(ShellModeActivationContext activationContext)
=> OnNavigatedTo(NavigationMode.New, activationContext);
void IShellClient.Deactivate()
=> OnNavigatedFrom(NavigationMode.New, null!);
Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem)
=> MenuItemInvokedOrSelectedAsync(menuItem);
Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
=> menuItem == null ? Task.CompletedTask : MenuItemInvokedOrSelectedAsync(menuItem);
2024-04-18 01:44:37 +02:00
}
2026-03-08 11:22:41 +01:00