Local draft resent and default app mode settings.

This commit is contained in:
Burak Kaan Köse
2026-02-22 17:55:57 +01:00
parent 311b3c77c8
commit 33672ab0aa
12 changed files with 229 additions and 30 deletions
@@ -52,6 +52,11 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary>
int EmailSyncIntervalMinutes { get; set; }
/// <summary>
/// Setting: Default application mode to open when activation does not specify one.
/// </summary>
WinoApplicationMode DefaultApplicationMode { get; set; }
#endregion
#region Mail
@@ -77,6 +77,7 @@
"Buttons_Save": "Save",
"Buttons_SaveConfiguration": "Save Configuration",
"Buttons_Send": "Send",
"Buttons_SendToServer": "Send to server",
"Buttons_Share": "Share",
"Buttons_SignIn": "Sign In",
"Buttons_Sync": "Synchronize",
@@ -619,6 +620,10 @@
"SettingsAppPreferences_SearchMode_Local": "Local",
"SettingsAppPreferences_SearchMode_Online": "Online",
"SettingsAppPreferences_SearchMode_Title": "Default search mode",
"SettingsAppPreferences_ApplicationMode_Title": "Default application mode",
"SettingsAppPreferences_ApplicationMode_Description": "Choose which mode Wino opens in when no activation type explicitly sets it.",
"SettingsAppPreferences_ApplicationMode_Mail": "Mail",
"SettingsAppPreferences_ApplicationMode_Calendar": "Calendar",
"SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.",
"SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background",
"SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.",
@@ -915,6 +920,7 @@
"Composer_CcBcc": "Cc & Bcc",
"Composer_EnableSmimeSignature": "Enable/disable S/MIME signature",
"Composer_EnableSmimeEncryption": "Enable/disable S/MIME encryption",
"Composer_LocalDraftSyncInfo": "This draft is local only. Wino failed to send it to your mail server. Click to retry sending it to the server.",
"Composer_CertificateExpires": "Expires on: ",
"Composer_SmimeSignature": "S/MIME Signature",
"Composer_SmimeEncryption": "S/MIME Encryption",
@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Messaging.UI;
namespace Wino.Core.Requests.Mail;
@@ -24,6 +22,7 @@ public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest
public override void RevertUIChanges()
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(Item));
// Keep local draft intact when create-draft synchronization fails.
// This allows users to retry sending the local draft to the server.
}
}
@@ -16,6 +16,9 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
[ObservableProperty]
public partial List<string> SearchModes { get; set; }
[ObservableProperty]
public partial List<string> ApplicationModes { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))]
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))]
@@ -48,6 +51,18 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
}
}
private string _selectedDefaultApplicationMode;
public string SelectedDefaultApplicationMode
{
get => _selectedDefaultApplicationMode;
set
{
SetProperty(ref _selectedDefaultApplicationMode, value);
PreferencesService.DefaultApplicationMode = (WinoApplicationMode)ApplicationModes.IndexOf(value);
}
}
private readonly IMailDialogService _dialogService;
private readonly IStartupBehaviorService _startupBehaviorService;
@@ -65,7 +80,14 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
Translator.SettingsAppPreferences_SearchMode_Online
];
ApplicationModes =
[
Translator.SettingsAppPreferences_ApplicationMode_Mail,
Translator.SettingsAppPreferences_ApplicationMode_Calendar
];
SelectedDefaultSearchMode = SearchModes[(int)PreferencesService.DefaultSearchMode];
SelectedDefaultApplicationMode = ApplicationModes[(int)PreferencesService.DefaultApplicationMode];
EmailSyncIntervalMinutes = PreferencesService.EmailSyncIntervalMinutes;
}
+140 -16
View File
@@ -15,6 +15,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
@@ -23,11 +24,15 @@ using Wino.Core.Services;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels;
public partial class ComposePageViewModel : MailBaseViewModel,
IRecipient<NewComposeDraftItemRequestedEvent>
IRecipient<NewComposeDraftItemRequestedEvent>,
IRecipient<SynchronizationActionsAdded>,
IRecipient<SynchronizationActionsCompleted>,
IRecipient<AccountSynchronizerStateChanged>
{
public Func<Task<string>> GetHTMLBodyFunction;
@@ -36,9 +41,11 @@ public partial class ComposePageViewModel : MailBaseViewModel,
private bool isUpdatingMimeBlocked = false;
private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy;
private bool canSendLocalDraftToServer => ComposingAccount != null && IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy && !IsRetryingSendToServer;
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
[ObservableProperty]
private MimeMessage currentMimeMessage = null;
@@ -50,15 +57,24 @@ public partial class ComposePageViewModel : MailBaseViewModel,
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsLocalDraft))]
[NotifyPropertyChangedFor(nameof(ShouldShowSendToServerButton))]
[NotifyPropertyChangedFor(nameof(ShouldShowSendButton))]
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
public partial MailItemViewModel CurrentMailDraftItem { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShouldShowSendToServerButton))]
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
public partial bool IsDraftBusy { get; set; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
public partial bool IsRetryingSendToServer { get; set; }
[ObservableProperty]
public partial bool IsImportanceSelected { get; set; }
@@ -74,6 +90,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
public partial MailAccount ComposingAccount { get; set; }
[ObservableProperty]
@@ -103,6 +120,8 @@ public partial class ComposePageViewModel : MailBaseViewModel,
public ObservableCollection<AccountContact> ToItems { get; set; } = [];
public ObservableCollection<AccountContact> CCItems { get; set; } = [];
public ObservableCollection<AccountContact> BCCItems { get; set; } = [];
public bool ShouldShowSendToServerButton => IsLocalDraft && !IsDraftBusy;
public bool ShouldShowSendButton => !IsLocalDraft;
#endregion
@@ -325,6 +344,48 @@ public partial class ComposePageViewModel : MailBaseViewModel,
await _worker.ExecuteAsync(draftSendPreparationRequest);
}
[RelayCommand(CanExecute = nameof(canSendLocalDraftToServer))]
private async Task SendToServerAsync()
{
if (CurrentMailDraftItem?.MailCopy == null || ComposingAccount == null || CurrentMimeMessage == null)
return;
try
{
await ExecuteUIThread(() =>
{
IsRetryingSendToServer = true;
IsDraftBusy = true;
NotifyComposeActionStateChanged();
});
await UpdateMimeChangesAsync().ConfigureAwait(false);
var localDraftCopy = CurrentMailDraftItem.MailCopy;
var draftPreparationRequest = new DraftPreparationRequest(
localDraftCopy.AssignedAccount ?? ComposingAccount,
localDraftCopy,
CurrentMimeMessage.GetBase64MimeMessage(),
DraftCreationReason.Empty);
await _worker.ExecuteAsync(draftPreparationRequest).ConfigureAwait(false);
}
catch (Exception ex)
{
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
}
finally
{
await ExecuteUIThread(() =>
{
IsRetryingSendToServer = false;
});
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
NotifyComposeActionStateChanged();
}
}
public async Task UpdateMimeChangesAsync()
{
if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
@@ -424,13 +485,13 @@ public partial class ComposePageViewModel : MailBaseViewModel,
}
}
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
base.OnNavigatedFrom(mode, parameters);
//public override void OnNavigatedFrom(NavigationMode mode, object parameters)
//{
// base.OnNavigatedFrom(mode, parameters);
/// Do not put any code here.
/// Make sure to use Page's OnNavigatedTo instead.
}
// /// Do not put any code here.
// /// Make sure to use Page's OnNavigatedTo instead.
//}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
@@ -461,11 +522,38 @@ public partial class ComposePageViewModel : MailBaseViewModel,
await TryPrepareComposeAsync(true);
}
public async void Receive(SynchronizationActionsAdded message)
{
if (!ShouldTrackDraftSynchronizationState(message.AccountId))
return;
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
}
public async void Receive(SynchronizationActionsCompleted message)
{
if (!ShouldTrackDraftSynchronizationState(message.AccountId))
return;
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
}
public async void Receive(AccountSynchronizerStateChanged message)
{
if (message.NewState != AccountSynchronizerState.Idle || !ShouldTrackDraftSynchronizationState(message.AccountId))
return;
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
}
protected override void RegisterRecipients()
{
base.RegisterRecipients();
Messenger.Register<NewComposeDraftItemRequestedEvent>(this);
Messenger.Register<SynchronizationActionsAdded>(this);
Messenger.Register<SynchronizationActionsCompleted>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this);
}
protected override void UnregisterRecipients()
@@ -473,6 +561,9 @@ public partial class ComposePageViewModel : MailBaseViewModel,
base.UnregisterRecipients();
Messenger.Unregister<NewComposeDraftItemRequestedEvent>(this);
Messenger.Unregister<SynchronizationActionsAdded>(this);
Messenger.Unregister<SynchronizationActionsCompleted>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
}
private async Task<bool> InitializeComposerAccountAsync()
@@ -514,19 +605,31 @@ public partial class ComposePageViewModel : MailBaseViewModel,
private async Task UpdatePendingOperationStateAsync()
{
IsDraftBusy = false;
var hasPendingOperation = false;
if (CurrentMailDraftItem?.MailCopy == null || !CurrentMailDraftItem.MailCopy.IsDraft)
{
await ExecuteUIThread(() =>
{
IsDraftBusy = false;
NotifyComposeActionStateChanged();
});
return;
}
var accountId = CurrentMailDraftItem.MailCopy.AssignedAccount?.Id ?? Guid.Empty;
if (accountId == Guid.Empty)
return;
if (accountId != Guid.Empty)
{
var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
hasPendingOperation = synchronizer?.HasPendingOperation(CurrentMailDraftItem.MailCopy.UniqueId) ?? false;
}
var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
IsDraftBusy = synchronizer?.HasPendingOperation(CurrentMailDraftItem.MailCopy.UniqueId) ?? false;
await ExecuteUIThread(() =>
{
IsDraftBusy = hasPendingOperation;
NotifyComposeActionStateChanged();
});
}
private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
@@ -706,11 +809,32 @@ public partial class ComposePageViewModel : MailBaseViewModel,
await ExecuteUIThread(async () =>
{
CurrentMailDraftItem.UpdateFrom(updatedMail);
DiscardCommand.NotifyCanExecuteChanged();
SendCommand.NotifyCanExecuteChanged();
await UpdatePendingOperationStateAsync();
NotifyComposeActionStateChanged();
});
}
}
private void NotifyComposeActionStateChanged()
{
OnPropertyChanged(nameof(IsLocalDraft));
OnPropertyChanged(nameof(ShouldShowSendToServerButton));
OnPropertyChanged(nameof(ShouldShowSendButton));
DiscardCommand.NotifyCanExecuteChanged();
SendCommand.NotifyCanExecuteChanged();
SendToServerCommand.NotifyCanExecuteChanged();
}
private bool ShouldTrackDraftSynchronizationState(Guid accountId)
{
if (accountId == Guid.Empty)
return false;
var currentDraftAccountId = CurrentMailDraftItem?.MailCopy?.AssignedAccount?.Id
?? ComposingAccount?.Id
?? Guid.Empty;
return currentDraftAccountId != Guid.Empty && currentDraftAccountId == accountId;
}
}
@@ -5,7 +5,7 @@ namespace Wino.Mail.WinUI.Activation;
internal static class AppModeActivationResolver
{
public static WinoApplicationMode Resolve(string? launchArguments, string? tileId, string? appId)
public static WinoApplicationMode Resolve(string? launchArguments, string? tileId, string? appId, WinoApplicationMode defaultMode = WinoApplicationMode.Mail)
{
if (TryResolveFromText(launchArguments, out var mode))
return mode;
@@ -16,7 +16,7 @@ internal static class AppModeActivationResolver
if (TryResolveFromText(appId, out mode))
return mode;
return WinoApplicationMode.Mail;
return defaultMode;
}
private static bool TryResolveFromText(string? value, out WinoApplicationMode mode)
+10 -6
View File
@@ -198,7 +198,10 @@ public partial class App : WinoApplication,
if (activationArgs.Kind == ExtendedActivationKind.AppNotification)
return true;
var launchMode = AppModeActivationResolver.Resolve(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine);
var launchMode = AppModeActivationResolver.Resolve(args?.Arguments,
GetCurrentLaunchTileId(),
Environment.CommandLine,
_preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail);
bool shouldRegister = launchMode == WinoApplicationMode.Mail;
if (!shouldRegister)
@@ -313,6 +316,7 @@ public partial class App : WinoApplication,
{
// Pass null for args since we're handling toast navigation
await CreateAndActivateWindow(null!);
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
}
else
{
@@ -453,7 +457,7 @@ public partial class App : WinoApplication,
return;
}
if (TryResolveActivationMode(activationArgs, out var activationMode))
if (TryResolveActivationMode(activationArgs, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var activationMode))
{
shellWindow.HandleAppActivation(GetModeLaunchArgument(activationMode));
return;
@@ -684,7 +688,7 @@ public partial class App : WinoApplication,
{
shellWindow.HandleAppActivation(launchArgs.Arguments, launchArgs.TileId);
}
else if (TryResolveActivationMode(args, out var redirectedMode))
else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode))
{
shellWindow.HandleAppActivation(GetModeLaunchArgument(redirectedMode));
}
@@ -700,9 +704,9 @@ public partial class App : WinoApplication,
private static string GetModeLaunchArgument(WinoApplicationMode mode)
=> mode == WinoApplicationMode.Calendar ? "--mode=calendar" : "--mode=mail";
private static bool TryResolveActivationMode(AppActivationArguments activationArgs, out WinoApplicationMode mode)
private static bool TryResolveActivationMode(AppActivationArguments activationArgs, WinoApplicationMode defaultMode, out WinoApplicationMode mode)
{
mode = WinoApplicationMode.Mail;
mode = defaultMode;
if (activationArgs.Kind == ExtendedActivationKind.Protocol &&
activationArgs.Data is IProtocolActivatedEventArgs protocolArgs)
@@ -746,7 +750,7 @@ public partial class App : WinoApplication,
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
{
mode = AppModeActivationResolver.Resolve(launchArgs.Arguments, launchArgs.TileId, null);
mode = AppModeActivationResolver.Resolve(launchArgs.Arguments, launchArgs.TileId, null, defaultMode);
return true;
}
@@ -290,6 +290,19 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(EmailSyncIntervalMinutes), value);
}
public WinoApplicationMode DefaultApplicationMode
{
get
{
var configuredMode = _configurationService.Get(nameof(DefaultApplicationMode), WinoApplicationMode.Mail);
return Enum.IsDefined(typeof(WinoApplicationMode), configuredMode)
? configuredMode
: WinoApplicationMode.Mail;
}
set => SaveProperty(propertyName: nameof(DefaultApplicationMode), value);
}
public CalendarSettings GetCurrentCalendarSettings()
{
var workingDays = GetDaysBetween(WorkingDayStart, WorkingDayEnd);
+1 -1
View File
@@ -111,7 +111,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
public void HandleAppActivation(string? launchArguments, string? tileId = null, string? appId = null)
{
var targetMode = AppModeActivationResolver.Resolve(launchArguments, tileId, appId);
var targetMode = AppModeActivationResolver.Resolve(launchArguments, tileId, appId, PreferencesService.DefaultApplicationMode);
_currentMode = targetMode;
_isApplyingActivationMode = true;
+13 -1
View File
@@ -227,11 +227,23 @@
<coreControls:WinoFontIcon Icon="Delete" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Command="{x:Bind ViewModel.SendCommand}" Label="{x:Bind domain:Translator.Buttons_Send}">
<AppBarButton
Command="{x:Bind ViewModel.SendCommand}"
Label="{x:Bind domain:Translator.Buttons_Send}"
Visibility="{x:Bind ViewModel.ShouldShowSendButton, Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Send" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
Command="{x:Bind ViewModel.SendToServerCommand}"
Label="{x:Bind domain:Translator.Buttons_SendToServer}"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_LocalDraftSyncInfo}"
Visibility="{x:Bind ViewModel.ShouldShowSendToServerButton, Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="SendNew" />
</AppBarButton.Icon>
</AppBarButton>
</toolkit:TabbedCommandBarItem>
</toolkit:TabbedCommandBar.PaneCustomContent>
<toolkit:TabbedCommandBar.MenuItems>
@@ -280,7 +280,14 @@ public sealed partial class MailListPage : MailListPageAbstract,
// No active mail item. Go to empty page.
if (message.SelectedMailItemViewModel == null)
{
WeakReferenceMessenger.Default.Send(new CancelRenderingContentRequested());
if (IsRenderingPageActive())
{
WeakReferenceMessenger.Default.Send(new CancelRenderingContentRequested());
}
// Ensure rendering frame actually navigates away from Compose/Rendering pages.
// Otherwise those pages keep their messenger registrations alive.
ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.DrillIn);
}
else
{
@@ -30,6 +30,13 @@
</controls:SettingsCard.HeaderIcon>
</controls:SettingsCard>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsAppPreferences_ApplicationMode_Description}" Header="{x:Bind domain:Translator.SettingsAppPreferences_ApplicationMode_Title}">
<ComboBox ItemsSource="{x:Bind ViewModel.ApplicationModes, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedDefaultApplicationMode, Mode=TwoWay}" />
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 2.5 3.125 C 2.5 2.955729 2.561849 2.809245 2.685547 2.685547 C 2.809244 2.56185 2.955729 2.5 3.125 2.5 L 16.875 2.5 C 17.044271 2.5 17.190756 2.56185 17.314453 2.685547 C 17.43815 2.809245 17.5 2.955729 17.5 3.125 L 17.5 16.875 C 17.5 17.044271 17.43815 17.190756 17.314453 17.314453 C 17.190756 17.43815 17.044271 17.5 16.875 17.5 L 3.125 17.5 C 2.955729 17.5 2.809244 17.43815 2.685547 17.314453 C 2.561849 17.190756 2.5 17.044271 2.5 16.875 Z M 3.75 3.75 L 3.75 16.25 L 16.25 16.25 L 16.25 3.75 Z M 9.375 5 C 9.375 4.830729 9.436849 4.684245 9.560547 4.560547 C 9.684244 4.43685 9.830729 4.375 10 4.375 C 10.169271 4.375 10.315755 4.43685 10.439453 4.560547 C 10.56315 4.684245 10.625 4.830729 10.625 5 L 10.625 15 C 10.625 15.169271 10.56315 15.315756 10.439453 15.439453 C 10.315755 15.56315 10.169271 15.625 10 15.625 C 9.830729 15.625 9.684244 15.56315 9.560547 15.439453 C 9.436849 15.315756 9.375 15.169271 9.375 15 Z " />
</controls:SettingsCard.HeaderIcon>
</controls:SettingsCard>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsAppPreferences_EmailSyncInterval_Description}" Header="{x:Bind domain:Translator.SettingsAppPreferences_EmailSyncInterval_Title}">
<NumberBox
Minimum="1"