From 4cb08f0a98f938a81fd41f9a22c069e9c0f65b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 11 Apr 2026 01:04:59 +0200 Subject: [PATCH] Implemented initial version for popping out window for rendering and compose pages --- .../Translations/en_US/resources.json | 1 + Wino.Mail.ViewModels/ComposePageViewModel.cs | 33 ++- .../MailRenderingPageViewModel.cs | 44 ++-- .../Models/ComposeDraftRequestedEventArgs.cs | 13 ++ .../Controls/OperationCommandBar.cs | 38 +++- .../HostedContentPopoutWindow.xaml | 23 +++ .../HostedContentPopoutWindow.xaml.cs | 46 +++++ .../Interfaces/IHostedPopoutSource.cs | 15 ++ Wino.Mail.WinUI/Interfaces/IPopoutClient.cs | 13 ++ .../Models/HostedPopoutDescriptor.cs | 10 + .../Models/PopOutRequestedEventArgs.cs | 8 + .../Models/PopoutHostActionKind.cs | 9 + .../PopoutHostActionRequestedEventArgs.cs | 17 ++ Wino.Mail.WinUI/Models/WinoWindowKind.cs | 3 +- .../HostedContentPopoutCoordinator.cs | 58 ++++++ Wino.Mail.WinUI/Services/NavigationService.cs | 10 +- Wino.Mail.WinUI/Views/Mail/ComposePage.xaml | 8 + .../Views/Mail/ComposePage.xaml.cs | 62 ++++-- Wino.Mail.WinUI/Views/Mail/MailListPage.xaml | 11 +- .../Views/Mail/MailListPage.xaml.cs | 188 +++++++++++++++++- .../Views/Mail/MailRenderingPage.xaml | 4 +- .../Views/Mail/MailRenderingPage.xaml.cs | 93 ++++++--- 22 files changed, 628 insertions(+), 79 deletions(-) create mode 100644 Wino.Mail.ViewModels/Models/ComposeDraftRequestedEventArgs.cs create mode 100644 Wino.Mail.WinUI/HostedContentPopoutWindow.xaml create mode 100644 Wino.Mail.WinUI/HostedContentPopoutWindow.xaml.cs create mode 100644 Wino.Mail.WinUI/Interfaces/IHostedPopoutSource.cs create mode 100644 Wino.Mail.WinUI/Interfaces/IPopoutClient.cs create mode 100644 Wino.Mail.WinUI/Models/HostedPopoutDescriptor.cs create mode 100644 Wino.Mail.WinUI/Models/PopOutRequestedEventArgs.cs create mode 100644 Wino.Mail.WinUI/Models/PopoutHostActionKind.cs create mode 100644 Wino.Mail.WinUI/Models/PopoutHostActionRequestedEventArgs.cs create mode 100644 Wino.Mail.WinUI/Services/HostedContentPopoutCoordinator.cs diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index d49740b2..8871c450 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -58,6 +58,7 @@ "Buttons_Allow": "Allow", "Buttons_Apply": "Apply", "Buttons_ApplyTheme": "Apply Theme", + "Buttons_PopOut": "Pop out", "Buttons_Browse": "Browse", "Buttons_Cancel": "Cancel", "Buttons_Close": "Close", diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 24a434f3..644ade4a 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -23,21 +23,22 @@ using Wino.Core.Domain.Models.Navigation; using Wino.Core.Extensions; 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, IRecipient, IRecipient, IRecipient { + public event EventHandler CloseRequested; + private static readonly TimeSpan LocalDraftRetryGracePeriod = TimeSpan.FromSeconds(15); public Func> GetHTMLBodyFunction; + public Func RenderHtmlBodyAsyncFunc { get; set; } public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) { @@ -529,9 +530,9 @@ public partial class ComposePageViewModel : MailBaseViewModel, } } - public async void Receive(ReaderItemRefreshRequestedEvent message) + public async Task RefreshDraftAsync(MailItemViewModel draftMailItemViewModel) { - if (message.MailItemViewModel == null || !message.MailItemViewModel.IsDraft) return; + if (draftMailItemViewModel == null || !draftMailItemViewModel.IsDraft) return; // Save current draft before switching. await UpdateMimeChangesAsync(); @@ -542,7 +543,7 @@ public partial class ComposePageViewModel : MailBaseViewModel, IncludedAttachments.Clear(); // Set the new draft item and prepare it. - CurrentMailDraftItem = message.MailItemViewModel; + CurrentMailDraftItem = draftMailItemViewModel; await UpdatePendingOperationStateAsync(); await LoadEmailTemplatesAsync(); await TryPrepareComposeAsync(true); @@ -591,7 +592,6 @@ public partial class ComposePageViewModel : MailBaseViewModel, { base.RegisterRecipients(); - Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); @@ -601,7 +601,6 @@ public partial class ComposePageViewModel : MailBaseViewModel, { base.UnregisterRecipients(); - Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); @@ -754,9 +753,12 @@ public partial class ComposePageViewModel : MailBaseViewModel, IsCCBCCVisible = true; Subject = replyingMime.Subject; - - Messenger.Send(new CreateNewComposeMailRequested(renderModel)); }); + + if (RenderHtmlBodyAsyncFunc != null) + { + await ExecuteUIThread(async () => await RenderHtmlBodyAsyncFunc(renderModel.RenderHtml)); + } } private void LoadAttachments() @@ -891,6 +893,19 @@ public partial class ComposePageViewModel : MailBaseViewModel, } } + protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source) + { + base.OnMailRemoved(removedMail, source); + + if (CurrentMailDraftItem?.MailCopy == null) + return; + + if (CurrentMailDraftItem.MailCopy.UniqueId != removedMail.UniqueId) + return; + + await ExecuteUIThread(() => CloseRequested?.Invoke(this, EventArgs.Empty)); + } + private void NotifyComposeActionStateChanged() { OnPropertyChanged(nameof(IsLocalDraft)); diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 70bea3cb..7e6aed25 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -26,7 +26,7 @@ using Wino.Core.Domain.Models.Printing; using Wino.Core.Domain.Models.Reader; using Wino.Core.Services; using Wino.Mail.ViewModels.Data; -using Wino.Mail.ViewModels.Messages; +using Wino.Mail.ViewModels.Models; using Wino.Messaging.Client.Mails; using Wino.Messaging.UI; using IMailService = Wino.Core.Domain.Interfaces.IMailService; @@ -34,10 +34,12 @@ using IMailService = Wino.Core.Domain.Interfaces.IMailService; namespace Wino.Mail.ViewModels; public partial class MailRenderingPageViewModel : MailBaseViewModel, - IRecipient, IRecipient, ITransferProgress // For listening IMAP message download progress. { + public event EventHandler CloseRequested; + public event EventHandler ComposeRequested; + private readonly IMailDialogService _dialogService; private readonly IUnderlyingThemeService _underlyingThemeService; @@ -58,8 +60,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // Func to get WebView2 to save current HTML as PDF to given location. // Used in 'Save as' and 'Print' functionality. public Func> SaveHTMLasPDFFunc { get; set; } - public Func> DirectPrintFuncAsync { get; set; } + public Func RenderHtmlAsyncFunc { get; set; } + public Func ClearRenderedHtmlAsyncFunc { get; set; } #region Properties @@ -319,6 +322,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); await _requestDelegator.ExecuteAsync(draftPreparationRequest); + ComposeRequested?.Invoke(this, new ComposeDraftRequestedEventArgs(draftMailCopy.UniqueId)); } else if (initializedMailItemViewModel != null) @@ -362,8 +366,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, initializedMimeMessageInformation = null; CurrentMailItemDisplayInformation = null; - // Dispose existing content first. - Messenger.Send(new CancelRenderingContentRequested()); + if (ClearRenderedHtmlAsyncFunc != null) + { + await ExecuteUIThread(async () => await ClearRenderedHtmlAsyncFunc()); + } // This page can be accessed for 2 purposes. // 1. Rendering a mail item when the user selects. @@ -509,8 +515,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, CurrentRenderModel = _mimeFileService.GetMailRenderModel(message, messagePath, renderingOptions); - Messenger.Send(new HtmlRenderingRequested(CurrentRenderModel.RenderHtml)); - foreach (var attachment in CurrentRenderModel.Attachments) { Attachments.Add(new MailAttachmentViewModel(attachment)); @@ -520,6 +524,11 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, StatePersistenceService.IsReadingMail = true; }); + + if (RenderHtmlAsyncFunc != null) + { + await ExecuteUIThread(async () => await RenderHtmlAsyncFunc(CurrentRenderModel.RenderHtml)); + } } private async Task> GetAccountContacts(InternetAddressList internetAddresses) @@ -658,6 +667,19 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, await ExecuteUIThread(() => { InitializeCommandBarItems(); }); } + protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source) + { + base.OnMailRemoved(removedMail, source); + + if (initializedMailItemViewModel?.MailCopy == null) + return; + + if (initializedMailItemViewModel.MailCopy.UniqueId != removedMail.UniqueId) + return; + + await ExecuteUIThread(() => CloseRequested?.Invoke(this, EventArgs.Empty)); + } + [RelayCommand] private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel) { @@ -794,13 +816,13 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // For upload. void ITransferProgress.Report(long bytesTransferred) { } - public async void Receive(ReaderItemRefreshRequestedEvent message) + public async Task RefreshMailItemAsync(MailItemViewModel mailItemViewModel) { - if (message.MailItemViewModel == null || message.MailItemViewModel.IsDraft) return; + if (mailItemViewModel == null || mailItemViewModel.IsDraft) return; try { - await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token); + await RenderAsync(mailItemViewModel, renderCancellationTokenSource.Token); } catch (OperationCanceledException) { @@ -907,7 +929,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, { base.RegisterRecipients(); - Messenger.Register(this); Messenger.Register(this); } @@ -915,7 +936,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, { base.UnregisterRecipients(); - Messenger.Unregister(this); Messenger.Unregister(this); } } diff --git a/Wino.Mail.ViewModels/Models/ComposeDraftRequestedEventArgs.cs b/Wino.Mail.ViewModels/Models/ComposeDraftRequestedEventArgs.cs new file mode 100644 index 00000000..eaa991bb --- /dev/null +++ b/Wino.Mail.ViewModels/Models/ComposeDraftRequestedEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Wino.Mail.ViewModels.Models; + +public sealed class ComposeDraftRequestedEventArgs : EventArgs +{ + public ComposeDraftRequestedEventArgs(Guid draftUniqueId) + { + DraftUniqueId = draftUniqueId; + } + + public Guid DraftUniqueId { get; } +} diff --git a/Wino.Mail.WinUI/Controls/OperationCommandBar.cs b/Wino.Mail.WinUI/Controls/OperationCommandBar.cs index 969e19f0..0da1fcf5 100644 --- a/Wino.Mail.WinUI/Controls/OperationCommandBar.cs +++ b/Wino.Mail.WinUI/Controls/OperationCommandBar.cs @@ -23,6 +23,7 @@ public sealed partial class OperationCommandBar : CommandBar private const string MailOperationTemplateKey = "OperationCommandBarMailOperationTemplate"; private const string FolderOperationTemplateKey = "OperationCommandBarFolderOperationTemplate"; private const string AIActionsTemplateKey = "OperationCommandBarAIActionsTemplate"; + private const string PopOutTemplateKey = "OperationCommandBarThemeToggleTemplate"; private const string ThemeToggleTemplateKey = "OperationCommandBarThemeToggleTemplate"; private const string SeparatorTemplateKey = "OperationCommandBarSeparatorTemplate"; @@ -47,7 +48,11 @@ public sealed partial class OperationCommandBar : CommandBar [GeneratedDependencyProperty] public partial bool IsEditorThemeToggleVisible { get; set; } + [GeneratedDependencyProperty] + public partial bool IsPopOutButtonVisible { get; set; } + public event EventHandler? AIActionsEnabledChanged; + public event EventHandler? PopOutClicked; public OperationCommandBar() { @@ -58,7 +63,6 @@ public sealed partial class OperationCommandBar : CommandBar OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Auto; Loaded += OnLoaded; - Unloaded += OnUnloaded; DynamicOverflowItemsChanging += OperationCommandBar_DynamicOverflowItemsChanging; } @@ -100,14 +104,14 @@ public sealed partial class OperationCommandBar : CommandBar RefreshCommands(); } - private void OnLoaded(object sender, RoutedEventArgs e) + partial void OnIsPopOutButtonVisibleChanged(bool newValue) { RefreshCommands(); } - private void OnUnloaded(object sender, RoutedEventArgs e) + private void OnLoaded(object sender, RoutedEventArgs e) { - ClearGeneratedCommands(); + RefreshCommands(); } private void OperationCommandBar_DynamicOverflowItemsChanging(CommandBar sender, DynamicOverflowItemsChangingEventArgs args) @@ -216,6 +220,11 @@ public sealed partial class OperationCommandBar : CommandBar PrimaryCommands.Add(CreateAIActionsToggleButton()); } + if (IsPopOutButtonVisible) + { + PrimaryCommands.Add(CreatePopOutButton()); + } + if (IsEditorThemeToggleVisible) { PrimaryCommands.Add(CreateThemeToggleButton()); @@ -266,6 +275,7 @@ public sealed partial class OperationCommandBar : CommandBar case AppBarButton button: button.Click -= OperationButton_Click; button.Click -= ThemeButton_Click; + button.Click -= PopOutButton_Click; break; case AppBarToggleButton toggleButton: toggleButton.ClearValue(AppBarToggleButton.IsCheckedProperty); @@ -356,6 +366,16 @@ public sealed partial class OperationCommandBar : CommandBar return button; } + private AppBarButton CreatePopOutButton() + { + var button = (AppBarButton)LoadCommandBarElementTemplate( + PopOutTemplateKey, + new OperationCommandBarThemeItemViewModel(Translator.Buttons_PopOut, WinoIconGlyph.OpenInNewWindow)); + + button.Click += PopOutButton_Click; + return button; + } + private void OperationButton_Click(object sender, RoutedEventArgs e) { if (sender is AppBarButton button && button.Tag is IMenuOperation operation) @@ -369,6 +389,11 @@ public sealed partial class OperationCommandBar : CommandBar IsEditorThemeDark = !IsEditorThemeDark; } + private void PopOutButton_Click(object sender, RoutedEventArgs e) + { + PopOutClicked?.Invoke(this, EventArgs.Empty); + } + private object? FindTemplateResource(string key) { if (TryGetResourceRecursive(Resources, key, out var resource)) @@ -430,6 +455,11 @@ public sealed partial class OperationCommandBar : CommandBar : CommandBarOverflowButtonVisibility.Auto; } + public void InvalidateCommands() + { + RefreshCommands(); + } + private sealed class SeparatorCommandBarItemViewModel; } diff --git a/Wino.Mail.WinUI/HostedContentPopoutWindow.xaml b/Wino.Mail.WinUI/HostedContentPopoutWindow.xaml new file mode 100644 index 00000000..d419aaa2 --- /dev/null +++ b/Wino.Mail.WinUI/HostedContentPopoutWindow.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/HostedContentPopoutWindow.xaml.cs b/Wino.Mail.WinUI/HostedContentPopoutWindow.xaml.cs new file mode 100644 index 00000000..f6b7adeb --- /dev/null +++ b/Wino.Mail.WinUI/HostedContentPopoutWindow.xaml.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.UI.Xaml; +using Wino.Mail.WinUI.Models; +using WinUIEx; + +namespace Wino.Mail.WinUI; + +public sealed partial class HostedContentPopoutWindow : WindowEx +{ + private readonly Action _closedCallback; + + public HostedPopoutDescriptor Descriptor { get; } + + public HostedContentPopoutWindow(HostedPopoutDescriptor descriptor, Action closedCallback) + { + Descriptor = descriptor; + _closedCallback = closedCallback; + + InitializeComponent(); + + Title = descriptor.Title; + Width = descriptor.Width; + Height = descriptor.Height; + MinWidth = descriptor.MinWidth; + MinHeight = descriptor.MinHeight; + + ExtendsContentIntoTitleBar = true; + + this.SetIcon("Assets/Wino_Icon.ico"); + this.CenterOnScreen(); + + Closed += OnClosed; + } + + public void SetHostedContent(FrameworkElement content) + { + ContentHost.Children.Clear(); + ContentHost.Children.Add(content); + } + + private void OnClosed(object sender, WindowEventArgs args) + { + Closed -= OnClosed; + _closedCallback(); + } +} diff --git a/Wino.Mail.WinUI/Interfaces/IHostedPopoutSource.cs b/Wino.Mail.WinUI/Interfaces/IHostedPopoutSource.cs new file mode 100644 index 00000000..81ce5f73 --- /dev/null +++ b/Wino.Mail.WinUI/Interfaces/IHostedPopoutSource.cs @@ -0,0 +1,15 @@ +using Microsoft.UI.Xaml; +using Wino.Mail.WinUI; +using Wino.Mail.WinUI.Models; + +namespace Wino.Mail.WinUI.Interfaces; + +public interface IHostedPopoutSource +{ + bool CanPopOutCurrentContent(); + FrameworkElement? GetCurrentHostedContent(); + HostedPopoutDescriptor CreatePopoutDescriptor(IPopoutClient client); + FrameworkElement DetachHostedContent(); + void OnHostedContentPoppedOut(FrameworkElement content, HostedContentPopoutWindow window, HostedPopoutDescriptor descriptor); + void OnHostedPopoutClosed(FrameworkElement content, HostedPopoutDescriptor descriptor); +} diff --git a/Wino.Mail.WinUI/Interfaces/IPopoutClient.cs b/Wino.Mail.WinUI/Interfaces/IPopoutClient.cs new file mode 100644 index 00000000..db37f168 --- /dev/null +++ b/Wino.Mail.WinUI/Interfaces/IPopoutClient.cs @@ -0,0 +1,13 @@ +using System; +using Wino.Mail.WinUI.Models; + +namespace Wino.Mail.WinUI.Interfaces; + +public interface IPopoutClient +{ + bool SupportsPopOut { get; } + event EventHandler PopOutRequested; + event EventHandler HostActionRequested; + HostedPopoutDescriptor GetPopoutDescriptor(); + void OnPopoutStateChanged(bool isPoppedOut); +} diff --git a/Wino.Mail.WinUI/Models/HostedPopoutDescriptor.cs b/Wino.Mail.WinUI/Models/HostedPopoutDescriptor.cs new file mode 100644 index 00000000..311d04bf --- /dev/null +++ b/Wino.Mail.WinUI/Models/HostedPopoutDescriptor.cs @@ -0,0 +1,10 @@ +namespace Wino.Mail.WinUI.Models; + +public sealed record HostedPopoutDescriptor( + string WindowName, + string Title, + double Width, + double Height, + double MinWidth, + double MinHeight, + string ContentKind); diff --git a/Wino.Mail.WinUI/Models/PopOutRequestedEventArgs.cs b/Wino.Mail.WinUI/Models/PopOutRequestedEventArgs.cs new file mode 100644 index 00000000..10ec980a --- /dev/null +++ b/Wino.Mail.WinUI/Models/PopOutRequestedEventArgs.cs @@ -0,0 +1,8 @@ +using System; + +namespace Wino.Mail.WinUI.Models; + +public sealed class PopOutRequestedEventArgs : EventArgs +{ + public static PopOutRequestedEventArgs Default { get; } = new(); +} diff --git a/Wino.Mail.WinUI/Models/PopoutHostActionKind.cs b/Wino.Mail.WinUI/Models/PopoutHostActionKind.cs new file mode 100644 index 00000000..31ad86ca --- /dev/null +++ b/Wino.Mail.WinUI/Models/PopoutHostActionKind.cs @@ -0,0 +1,9 @@ +using System; + +namespace Wino.Mail.WinUI.Models; + +public enum PopoutHostActionKind +{ + CloseHostedInstance, + PopOutNextNavigation +} diff --git a/Wino.Mail.WinUI/Models/PopoutHostActionRequestedEventArgs.cs b/Wino.Mail.WinUI/Models/PopoutHostActionRequestedEventArgs.cs new file mode 100644 index 00000000..608b8fa0 --- /dev/null +++ b/Wino.Mail.WinUI/Models/PopoutHostActionRequestedEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace Wino.Mail.WinUI.Models; + +public sealed class PopoutHostActionRequestedEventArgs : EventArgs +{ + public PopoutHostActionRequestedEventArgs(PopoutHostActionKind actionKind, Type? targetPageType = null, Guid? targetMailUniqueId = null) + { + ActionKind = actionKind; + TargetPageType = targetPageType; + TargetMailUniqueId = targetMailUniqueId; + } + + public PopoutHostActionKind ActionKind { get; } + public Type? TargetPageType { get; } + public Guid? TargetMailUniqueId { get; } +} diff --git a/Wino.Mail.WinUI/Models/WinoWindowKind.cs b/Wino.Mail.WinUI/Models/WinoWindowKind.cs index 6a670d7e..48dc0045 100644 --- a/Wino.Mail.WinUI/Models/WinoWindowKind.cs +++ b/Wino.Mail.WinUI/Models/WinoWindowKind.cs @@ -3,5 +3,6 @@ namespace Wino.Mail.WinUI.Models; public enum WinoWindowKind { Shell, - Welcome + Welcome, + HostedPopout } diff --git a/Wino.Mail.WinUI/Services/HostedContentPopoutCoordinator.cs b/Wino.Mail.WinUI/Services/HostedContentPopoutCoordinator.cs new file mode 100644 index 00000000..57491e42 --- /dev/null +++ b/Wino.Mail.WinUI/Services/HostedContentPopoutCoordinator.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Wino.Core.Domain.Interfaces; +using Wino.Mail.WinUI.Interfaces; +using Wino.Mail.WinUI.Models; + +namespace Wino.Mail.WinUI.Services; + +public static class HostedContentPopoutCoordinator +{ + public static async Task PopOutCurrentContentAsync(IHostedPopoutSource source) + { + if (!source.CanPopOutCurrentContent()) + return false; + + var content = source.GetCurrentHostedContent(); + if (content is not FrameworkElement frameworkElement || frameworkElement is not IPopoutClient client || !client.SupportsPopOut) + return false; + + var descriptor = source.CreatePopoutDescriptor(client); + var windowManager = WinoApplication.Current.Services.GetRequiredService(); + + if (windowManager.GetWindow(WinoWindowKind.HostedPopout, descriptor.WindowName) is HostedContentPopoutWindow existingWindow) + { + windowManager.ActivateWindow(existingWindow); + return false; + } + + var detachedContent = source.DetachHostedContent(); + if (detachedContent is IPopoutClient detachedClient) + { + detachedClient.OnPopoutStateChanged(true); + } + + HostedContentPopoutWindow? popoutWindow = null; + + popoutWindow = (HostedContentPopoutWindow)windowManager.CreateWindow( + WinoWindowKind.HostedPopout, + () => new HostedContentPopoutWindow(descriptor, () => + { + source.OnHostedPopoutClosed(detachedContent, descriptor); + }), + descriptor.WindowName); + + popoutWindow.SetHostedContent(detachedContent); + source.OnHostedContentPoppedOut(detachedContent, popoutWindow, descriptor); + windowManager.ActivateWindow(popoutWindow); + + var themeService = WinoApplication.Current.Services.GetService(); + if (themeService != null) + { + await themeService.ApplyThemeToActiveWindowAsync(); + } + + return true; + } +} diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index d6d06dd2..7c4688ed 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -381,7 +381,10 @@ public class NavigationService : NavigationServiceBase, INavigationService && parameter is MailItemViewModel mailItemViewModel && page != WinoPage.ComposePage) { - WeakReferenceMessenger.Default.Send(new ReaderItemRefreshRequestedEvent(mailItemViewModel)); + if (listingFrame.Content is MailRenderingPage renderingPage) + { + _ = renderingPage.RefreshMailItemAsync(mailItemViewModel); + } } else if (listingFrame.Content != null && listingFrame.Content.GetType() == GetPageType(WinoPage.ComposePage) @@ -390,7 +393,10 @@ public class NavigationService : NavigationServiceBase, INavigationService { // ComposePage is already active and we're switching to another draft. // Reuse existing ComposePage and WebView2 instead of navigating. - WeakReferenceMessenger.Default.Send(new ReaderItemRefreshRequestedEvent(composeDraftViewModel)); + if (listingFrame.Content is ComposePage composePage) + { + _ = composePage.RefreshDraftAsync(composeDraftViewModel); + } } else if (listingFrame.Content != null && listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage) diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml index d2a5d332..10d1887c 100644 --- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml +++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml @@ -172,6 +172,14 @@ + + + + + diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs index 8056dbdc..43bedfe5 100644 --- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs @@ -25,6 +25,8 @@ using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; using Wino.Mail.WinUI.Controls; using Wino.Mail.WinUI.Extensions; +using Wino.Mail.WinUI.Interfaces; +using Wino.Mail.WinUI.Models; using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Shell; using Wino.Views.Abstract; @@ -33,13 +35,19 @@ namespace Wino.Views.Mail; public sealed partial class ComposePage : ComposePageAbstract, IAiHtmlActionHost, - IRecipient, - IRecipient, - IRecipient + IPopoutClient, + IRecipient { + private bool _isPoppedOut; + + public bool SupportsPopOut => !_isPoppedOut; + public event EventHandler? PopOutRequested; + public event EventHandler? HostActionRequested; + public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView(); public Visibility GetAiActionsToggleVisibility(bool isHidden) => isHidden ? Visibility.Collapsed : Visibility.Visible; + public Visibility GetPopOutButtonVisibility() => SupportsPopOut ? Visibility.Visible : Visibility.Collapsed; public Visibility GetAiActionsPanelVisibility(bool? isChecked, bool isHidden) => !isHidden && isChecked == true ? Visibility.Visible : Visibility.Collapsed; @@ -49,6 +57,28 @@ public sealed partial class ComposePage : ComposePageAbstract, public ComposePage() { InitializeComponent(); + ViewModel.CloseRequested += ViewModel_CloseRequested; + } + + public HostedPopoutDescriptor GetPopoutDescriptor() + { + var title = string.IsNullOrWhiteSpace(ViewModel.Subject) ? Translator.Draft : ViewModel.Subject; + var draftId = ViewModel.CurrentMailDraftItem?.MailCopy?.UniqueId.ToString("N") ?? title; + + return new HostedPopoutDescriptor( + $"compose-{draftId}", + title, + 1180, + 860, + 760, + 600, + nameof(ComposePage)); + } + + public void OnPopoutStateChanged(bool isPoppedOut) + { + _isPoppedOut = isPoppedOut; + Bindings.Update(); } public WinoIconGlyph GetEditorThemeIcon(bool isDarkMode) => isDarkMode ? WinoIconGlyph.LightEditor : WinoIconGlyph.DarkEditor; @@ -277,11 +307,7 @@ public sealed partial class ComposePage : ComposePageAbstract, _disposables.Add(WebViewEditor); ViewModel.GetHTMLBodyFunction = WebViewEditor.GetHtmlBodyAsync; - } - - async void IRecipient.Receive(CreateNewComposeMailRequested message) - { - await WebViewEditor.RenderHtmlAsync(message.RenderModel.RenderHtml); + ViewModel.RenderHtmlBodyAsyncFunc = WebViewEditor.RenderHtmlAsync; } private void ShowCCBCCClicked(object sender, RoutedEventArgs e) @@ -289,6 +315,16 @@ public sealed partial class ComposePage : ComposePageAbstract, ViewModel.IsCCBCCVisible = true; } + private void PopOutButton_Click(object sender, RoutedEventArgs e) + { + PopOutRequested?.Invoke(this, PopOutRequestedEventArgs.Default); + } + + private void ViewModel_CloseRequested(object? sender, EventArgs e) + { + HostActionRequested?.Invoke(this, new PopoutHostActionRequestedEventArgs(PopoutHostActionKind.CloseHostedInstance)); + } + private async void ComposeAiActionsToggleButton_Checked(object sender, RoutedEventArgs e) { await ComposeAiActionsPanel.RefreshAvailabilityAsync(); @@ -333,12 +369,13 @@ public sealed partial class ComposePage : ComposePageAbstract, WebViewEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark; } - void IRecipient.Receive(ReaderItemRefreshRequestedEvent message) + public async Task RefreshDraftAsync(MailItemViewModel draftMailItemViewModel) { - if (message.MailItemViewModel == null || !message.MailItemViewModel.IsDraft) return; + if (draftMailItemViewModel == null || !draftMailItemViewModel.IsDraft) return; // Reset the initial focus flag so ToBox gets focus for the new draft. isInitialFocusHandled = false; + await ViewModel.RefreshDraftAsync(draftMailItemViewModel); } private void ImportanceClicked(object sender, RoutedEventArgs e) @@ -423,6 +460,7 @@ public sealed partial class ComposePage : ComposePageAbstract, FocusManager.GotFocus -= GlobalFocusManagerGotFocus; ComposeAiActionsPanel.CancelPendingOperation(); await ViewModel.UpdateMimeChangesAsync(); + ViewModel.RenderHtmlBodyAsyncFunc = null; DisposeDisposables(); } @@ -496,18 +534,14 @@ public sealed partial class ComposePage : ComposePageAbstract, { base.RegisterRecipients(); - WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); } protected override void UnregisterRecipients() { base.UnregisterRecipients(); - WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); - WeakReferenceMessenger.Default.Unregister(this); } // TODO: Save mime on closing the app. diff --git a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml index fac7b3f0..16283c00 100644 --- a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml +++ b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml @@ -176,12 +176,11 @@ - + OverflowButtonVisibility="Auto" /> @@ -359,7 +358,7 @@ HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal" - toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0" + toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="-6" CanDragItems="True" ChoosingItemContainer="WinoListViewChoosingItemContainer" IsItemClickEnabled="True" @@ -377,7 +376,7 @@ - + @@ -488,7 +487,7 @@ + Margin="0,3,0,0"> diff --git a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs index 38993a19..794e136d 100644 --- a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs @@ -28,8 +28,10 @@ using Wino.Mail.ViewModels.Messages; using Wino.Mail.WinUI; using Wino.Mail.WinUI.Controls.ListView; using Wino.Mail.WinUI.Extensions; +using Wino.Mail.WinUI.Helpers; using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Models; +using Wino.Mail.WinUI.Services; using Wino.MenuFlyouts.Context; using Wino.Messaging.Client.Mails; using Wino.Views.Abstract; @@ -48,12 +50,16 @@ public sealed partial class MailListPage : MailListPageAbstract, IRecipient, IRecipient, IRecipient, + IHostedPopoutSource, ITitleBarSearchHost { private const double RENDERING_COLUMN_MIN_WIDTH = 375; private const int SELECTION_SETTLE_DELAY_MS = 120; private const int RENDERING_FRAME_RELEASE_DELAY_MS = 2000; private int _idleNavigationRequestVersion = 0; + private IPopoutClient? _activePopoutClient; + private readonly Dictionary _hostedPopoutWindows = []; + private PendingHostedPopoutNavigation? _pendingHostedPopoutNavigation; private IStatePersistanceService StatePersistenceService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}"); private IKeyPressService KeyPressService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}"); @@ -69,6 +75,7 @@ public sealed partial class MailListPage : MailListPageAbstract, public MailListPage() { InitializeComponent(); + RenderingFrame.Navigated += RenderingFrame_Navigated; } protected override void OnNavigatedTo(NavigationEventArgs e) @@ -101,6 +108,7 @@ public sealed partial class MailListPage : MailListPageAbstract, base.OnNavigatedFrom(e); InvalidatePendingIdleNavigation(); + DetachPopoutClient(); this.Bindings.StopTracking(); @@ -325,7 +333,10 @@ public sealed partial class MailListPage : MailListPageAbstract, PrepareRenderingPageWebViewTransition(); // Dispose existing HTML content from rendering page webview. - WeakReferenceMessenger.Default.Send(new CancelRenderingContentRequested()); + if (RenderingFrame.Content is MailRenderingPage renderingPage) + { + _ = renderingPage.ClearRenderedContentAsync(); + } } else if (IsComposingPageActive()) { @@ -359,6 +370,55 @@ public sealed partial class MailListPage : MailListPageAbstract, private bool IsRenderingPageActive() => RenderingFrame.Content is MailRenderingPage; private bool IsComposingPageActive() => RenderingFrame.Content is ComposePage; + private void RenderingFrame_Navigated(object sender, NavigationEventArgs e) + { + AttachPopoutClient(RenderingFrame.Content as IPopoutClient); + + if (_pendingHostedPopoutNavigation != null + && TryGetPendingHostedPopoutTarget(RenderingFrame.Content, _pendingHostedPopoutNavigation, out var hostedContent)) + { + _ = ContinuePendingHostedPopoutNavigationAsync(hostedContent, _pendingHostedPopoutNavigation); + } + } + + private void AttachPopoutClient(IPopoutClient? client) + { + if (ReferenceEquals(_activePopoutClient, client)) + return; + + DetachPopoutClient(); + + _activePopoutClient = client; + if (_activePopoutClient != null) + { + _activePopoutClient.PopOutRequested += ActivePopoutClient_PopOutRequested; + _activePopoutClient.HostActionRequested += ActivePopoutClient_HostActionRequested; + } + } + + private void DetachPopoutClient() + { + if (_activePopoutClient != null) + { + _activePopoutClient.PopOutRequested -= ActivePopoutClient_PopOutRequested; + _activePopoutClient.HostActionRequested -= ActivePopoutClient_HostActionRequested; + _activePopoutClient = null; + } + } + + private async void ActivePopoutClient_PopOutRequested(object? sender, PopOutRequestedEventArgs e) + { + await HostedContentPopoutCoordinator.PopOutCurrentContentAsync(this); + } + + private void ActivePopoutClient_HostActionRequested(object? sender, PopoutHostActionRequestedEventArgs e) + { + if (sender is FrameworkElement content) + { + HandleHostedClientAction(content, e); + } + } + private void InvalidatePendingIdleNavigation() { unchecked @@ -378,7 +438,10 @@ public sealed partial class MailListPage : MailListPageAbstract, if (IsRenderingPageActive()) { - WeakReferenceMessenger.Default.Send(new CancelRenderingContentRequested()); + if (RenderingFrame.Content is MailRenderingPage renderingPage) + { + _ = renderingPage.ClearRenderedContentAsync(); + } } await Task.Delay(RENDERING_FRAME_RELEASE_DELAY_MS); @@ -926,4 +989,125 @@ public sealed partial class MailListPage : MailListPageAbstract, return Task.CompletedTask; } + + public bool CanPopOutCurrentContent() + { + return RenderingFrame.Content is FrameworkElement + && RenderingFrame.Content is IPopoutClient client + && client.SupportsPopOut; + } + + public FrameworkElement? GetCurrentHostedContent() + { + return RenderingFrame.Content as FrameworkElement; + } + + public HostedPopoutDescriptor CreatePopoutDescriptor(IPopoutClient client) + { + return client.GetPopoutDescriptor(); + } + + public FrameworkElement DetachHostedContent() + { + if (RenderingFrame.Content is not FrameworkElement content) + throw new InvalidOperationException("RenderingFrame does not host detachable content."); + + InvalidatePendingIdleNavigation(); + DetachPopoutClient(); + RenderingFrame.Content = null; + ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.None); + + return content; + } + + public void OnHostedContentPoppedOut(FrameworkElement content, HostedContentPopoutWindow window, HostedPopoutDescriptor descriptor) + { + if (content is IPopoutClient client) + { + client.HostActionRequested -= ActivePopoutClient_HostActionRequested; + client.HostActionRequested += ActivePopoutClient_HostActionRequested; + } + + _hostedPopoutWindows[content] = window; + _ = ViewModel.MailCollection.UnselectAllAsync(); + UpdateAdaptiveness(); + } + + public void OnHostedPopoutClosed(FrameworkElement content, HostedPopoutDescriptor descriptor) + { + if (_hostedPopoutWindows.Remove(content) && content is IPopoutClient hostedClient) + { + hostedClient.HostActionRequested -= ActivePopoutClient_HostActionRequested; + } + + if (_pendingHostedPopoutNavigation?.SourceContent == content) + { + _pendingHostedPopoutNavigation = null; + } + + DispatcherQueue.TryEnqueue(() => + { + if (content is IPopoutClient client) + { + client.OnPopoutStateChanged(false); + } + + WindowCleanupHelper.CleanupObject(content); + }); + } + + private void HandleHostedClientAction(FrameworkElement content, PopoutHostActionRequestedEventArgs args) + { + if (!_hostedPopoutWindows.TryGetValue(content, out var hostedWindow)) + return; + + switch (args.ActionKind) + { + case PopoutHostActionKind.CloseHostedInstance: + hostedWindow.Close(); + break; + case PopoutHostActionKind.PopOutNextNavigation when args.TargetPageType != null: + _pendingHostedPopoutNavigation = new PendingHostedPopoutNavigation(content, hostedWindow, args.TargetPageType, args.TargetMailUniqueId); + break; + } + } + + private static bool TryGetPendingHostedPopoutTarget(object? currentContent, PendingHostedPopoutNavigation pendingHostedNavigation, out FrameworkElement hostedContent) + { + hostedContent = null!; + + if (currentContent is not FrameworkElement currentFrameworkElement || currentFrameworkElement.GetType() != pendingHostedNavigation.TargetPageType) + return false; + + if (pendingHostedNavigation.TargetMailUniqueId.HasValue + && currentFrameworkElement is ComposePage composePage + && composePage.ViewModel.CurrentMailDraftItem?.MailCopy?.UniqueId != pendingHostedNavigation.TargetMailUniqueId.Value) + { + return false; + } + + hostedContent = currentFrameworkElement; + return true; + } + + private async Task ContinuePendingHostedPopoutNavigationAsync(FrameworkElement content, PendingHostedPopoutNavigation pendingHostedNavigation) + { + if (!ReferenceEquals(_pendingHostedPopoutNavigation, pendingHostedNavigation)) + return; + + _pendingHostedPopoutNavigation = null; + + var didPopOut = await HostedContentPopoutCoordinator.PopOutCurrentContentAsync(this); + + if (didPopOut) + { + pendingHostedNavigation.SourceWindow.Close(); + } + } + + private sealed record PendingHostedPopoutNavigation( + FrameworkElement SourceContent, + HostedContentPopoutWindow SourceWindow, + Type TargetPageType, + Guid? TargetMailUniqueId); } diff --git a/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml index 87f6bc99..b0834a78 100644 --- a/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml +++ b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml @@ -272,8 +272,10 @@ IsAIActionsPaneToggleVisible="{x:Bind GetAiActionsToggleVisible(ViewModel.PreferencesService.IsAiActionsPanelHidden), Mode=OneWay}" IsEditorThemeDark="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=TwoWay}" IsEditorThemeToggleVisible="True" + IsPopOutButtonVisible="{x:Bind SupportsPopOut, Mode=OneWay}" ItemInvokedCommand="{x:Bind ViewModel.OperationClickedCommand}" - MenuItems="{x:Bind ViewModel.MenuItems, Mode=OneWay}"> + MenuItems="{x:Bind ViewModel.MenuItems, Mode=OneWay}" + PopOutClicked="RendererCommandBar_PopOutClicked"> diff --git a/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs index dc70bca9..6db2e604 100644 --- a/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs @@ -16,9 +16,12 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Printing; using Wino.Mail.ViewModels.Data; +using Wino.Mail.ViewModels.Models; using Wino.Mail.WinUI; using Wino.Mail.WinUI.Controls; using Wino.Mail.WinUI.Extensions; +using Wino.Mail.WinUI.Interfaces; +using Wino.Mail.WinUI.Models; using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Shell; using Wino.Views.Abstract; @@ -27,8 +30,7 @@ namespace Wino.Views.Mail; public sealed partial class MailRenderingPage : MailRenderingPageAbstract, IAiHtmlActionHost, - IRecipient, - IRecipient, + IPopoutClient, IRecipient { private readonly IPreferencesService _preferencesService = App.Current.Services.GetService()!; @@ -39,6 +41,11 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, private bool? _lastAppliedDarkTheme; private TaskCompletionSource DOMLoadedTask = new TaskCompletionSource(); private string _currentRenderedHtml = string.Empty; + private bool _isPoppedOut; + + public bool SupportsPopOut => !_isPoppedOut; + public event EventHandler? PopOutRequested; + public event EventHandler? HostActionRequested; public WebView2 GetWebView() => Chromium; public bool GetAiActionsToggleVisible(bool isHidden) => !isHidden; @@ -57,9 +64,34 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, { return Chromium.CoreWebView2.PrintToPdfAsync(path, null).AsTask(); }); + ViewModel.RenderHtmlAsyncFunc = RenderInternalAsync; + ViewModel.ClearRenderedHtmlAsyncFunc = ClearRenderedContentAsync; + ViewModel.CloseRequested += ViewModel_CloseRequested; + ViewModel.ComposeRequested += ViewModel_ComposeRequested; } + public HostedPopoutDescriptor GetPopoutDescriptor() + { + var title = string.IsNullOrWhiteSpace(ViewModel.Subject) ? Translator.MailItemNoSubject : ViewModel.Subject; + var uniquePart = ViewModel.CurrentMailFileId?.ToString("N") ?? title; + return new HostedPopoutDescriptor( + $"mail-rendering-{uniquePart}", + title, + 1080, + 780, + 640, + 480, + nameof(MailRenderingPage)); + } + + public void OnPopoutStateChanged(bool isPoppedOut) + { + _isPoppedOut = isPoppedOut; + Bindings.Update(); + RendererCommandBar.InvalidateCommands(); + } + private async Task DirectPrintAsync(WebView2PrintSettingsModel settings) { if (Chromium.CoreWebView2 == null) return PrintingResult.Failed; @@ -132,20 +164,14 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, private void DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args) => DOMLoadedTask.TrySetResult(true); - async void IRecipient.Receive(HtmlRenderingRequested message) + public async Task ClearRenderedContentAsync() { - // Ensure WebView2 is fully initialized before first render. - // OnNavigatedTo starts initialization fire-and-forget; this await - // guarantees the core is ready before we invoke any script. await EnsureChromiumInitializedAsync(); - if (message == null || string.IsNullOrEmpty(message.HtmlBody)) + if (!isRenderingInProgress) { await RenderInternalAsync(string.Empty); - return; } - - await RenderInternalAsync(message.HtmlBody); } protected override void OnNavigatedFrom(NavigationEventArgs e) @@ -157,8 +183,11 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, ViewModel.SaveHTMLasPDFFunc = null; ViewModel.DirectPrintFuncAsync = null; + ViewModel.RenderHtmlAsyncFunc = null; + ViewModel.ClearRenderedHtmlAsyncFunc = null; _currentRenderedHtml = string.Empty; RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged; + RendererCommandBar.PopOutClicked -= RendererCommandBar_PopOutClicked; RendererCommandBar.IsAIActionsEnabled = false; ReaderAiActionsPanel.CancelPendingOperation(); @@ -178,6 +207,11 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, cancellationToken.ThrowIfCancellationRequested(); } + public Task RefreshMailItemAsync(MailItemViewModel mailItemViewModel) + { + return ViewModel.RefreshMailItemAsync(mailItemViewModel); + } + private async void RendererCommandBar_AIActionsEnabledChanged(object? sender, bool isEnabled) { if (isEnabled) @@ -275,11 +309,13 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, protected override void OnNavigatedTo(NavigationEventArgs e) { - // Initialize WebView2 wiring before base navigation invokes ViewModel rendering. - // Base.OnNavigatedTo triggers VM.OnNavigatedTo, which can send HtmlRenderingRequested. DOMLoadedTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + ViewModel.RenderHtmlAsyncFunc = RenderInternalAsync; + ViewModel.ClearRenderedHtmlAsyncFunc = ClearRenderedContentAsync; RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged; RendererCommandBar.AIActionsEnabledChanged += RendererCommandBar_AIActionsEnabledChanged; + RendererCommandBar.PopOutClicked -= RendererCommandBar_PopOutClicked; + RendererCommandBar.PopOutClicked += RendererCommandBar_PopOutClicked; RendererCommandBar.IsAIActionsEnabled = false; Chromium.CoreWebView2Initialized -= CoreWebViewInitialized; Chromium.CoreWebView2Initialized += CoreWebViewInitialized; @@ -316,17 +352,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, Chromium.Source = new Uri("https://wino.mail/reader.html"); } - - async void IRecipient.Receive(CancelRenderingContentRequested message) - { - await EnsureChromiumInitializedAsync(); - - if (!isRenderingInProgress) - { - await RenderInternalAsync(string.Empty); - } - } - private async void WebViewNavigationStarting(WebView2 sender, CoreWebView2NavigationStartingEventArgs args) { // This is our reader. @@ -397,7 +422,8 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, private void InternetAddressClicked(object sender, RoutedEventArgs e) { - if (sender is HyperlinkButton hyperlinkButton) + // TODO: Popped out windows don't have xaml root assigned properly, therefore ShowAt will fail. + if (sender is HyperlinkButton hyperlinkButton && !_isPoppedOut) { hyperlinkButton.ContextFlyout.ShowAt(hyperlinkButton); } @@ -411,6 +437,21 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, } } + private void RendererCommandBar_PopOutClicked(object? sender, EventArgs e) + { + PopOutRequested?.Invoke(this, PopOutRequestedEventArgs.Default); + } + + private void ViewModel_CloseRequested(object? sender, EventArgs e) + { + HostActionRequested?.Invoke(this, new PopoutHostActionRequestedEventArgs(PopoutHostActionKind.CloseHostedInstance)); + } + + private void ViewModel_ComposeRequested(object? sender, ComposeDraftRequestedEventArgs e) + { + HostActionRequested?.Invoke(this, new PopoutHostActionRequestedEventArgs(PopoutHostActionKind.PopOutNextNavigation, typeof(ComposePage), e.DraftUniqueId)); + } + private void OpenAttachment_Click(object sender, RoutedEventArgs e) { if (sender is MenuFlyoutItem item && item.CommandParameter is MailAttachmentViewModel attachment) @@ -431,8 +472,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, { base.RegisterRecipients(); - WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); } @@ -440,8 +479,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, { base.UnregisterRecipients(); - WeakReferenceMessenger.Default.Unregister(this); - WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); }