Some more cleanup.

This commit is contained in:
Burak Kaan Köse
2026-04-05 13:18:50 +02:00
parent c1ab49fb1d
commit ca19297b92
22 changed files with 444 additions and 302 deletions
+147 -122
View File
@@ -1,133 +1,158 @@
# Wino Mail vNext Release Notes (vs. `main`) # Wino Mail vNext Improvements
This document summarizes the areas that were merged and improved on `feature/vNext` relative to `main`, based on commit history. This document summarizes the major improvements on `feature/vNext` compared to `main`, based on the commit history between the current branch and the merge-base with `main`.
## Major merged/improved areas ## Wino Calendar
### 1) Calendar management and scheduling experience Calendar has grown from an early implementation into a much more complete product area on this branch.
Calendar support was significantly expanded from foundational UI work to full account-integrated flows. The cycle includes calendar/mail mapping, richer calendar visuals, event details improvements, recurring occurrence summary work, and dedicated event compose/create flows. Calendar reliability also improved through delta-sync fixes, timezone correctness updates, duplicate-operation prevention, and better synchronization state handling across providers. Reminder handling became much more complete with snooze support spanning toast UX, service logic, and database persistence. Overall, calendar now behaves like a first-class product surface rather than an auxiliary feature.
### 2) Contact management and people-centric UX ### A full Wino Calendar experience
Contacts moved from incremental UI adjustments to robust management features across account and settings surfaces. The branch includes explicit contact-management commits, contact/settings integration updates, and data-model cleanup that removes legacy base64 contact storage patterns. Visual quality improvements (such as profile-image initials behavior and image preview controls) tightened the identity and people experience. In practice, this reduces friction when browsing and maintaining contact data and makes account-related contact operations more predictable.
### 3) Synchronization architecture, correctness, and resilience - Added a dedicated Wino Calendar app entry, making calendar a first-class experience instead of a secondary add-on.
Synchronization paths were deeply refactored to address long-standing reliability issues. The branch adds generic error handling (including 404 and Outlook 429 handling), improves thread mapping across synchronizers, introduces explicit mail/calendar synchronizer state, and hardens CalDAV/IMAP behaviors with targeted fixes. Operation safety improved via better busy-state handling, duplicate-operation avoidance, and execution-error handling in rendering/operation pipelines. These changes collectively reduce failure surfaces and improve consistency under real-world server and network conditions. - Built out the calendar rendering experience with multiple rounds of rendering improvements, updated calendar view styling, calendar buttons, and better event visuals.
- Added event creation and full event compose flows, including follow-up improvements for attachments, attendees, recurrence summaries, RSVP actions, reminders, and event details.
- Improved support for all-day events, better display dates, occurrence handling, and mail-to-calendar mapping so calendar actions connect more naturally with messages and invitations.
### 4) Compose/editor, rendering, and draft pipeline improvements ### Local calendar support
Message composition and rendering received notable architectural cleanup. Work includes editor and toolbar refactors, message-based compose/render simplifications, Gmail drafting, and large Outlook attachment support through upload sessions. Local-draft behavior was refined with resend logic and grace-period protections, while MIME/header and template work improved message fidelity. Together, these changes make authoring and sending mail more stable for both common and heavy payload workflows.
### 5) Performance, data integrity, and test/build quality - Added local calendar operation coverage and supporting behavior for IMAP-backed/local calendar scenarios.
The cycle introduced meaningful backend and tooling quality improvements: batch DB query mail fetching with in-memory caching, SQLite index additions, and foreign-key enforcement. Collection/thread update performance was optimized, and multiple targeted tests were added for calendar, IMAP, CalDAV, sanitization, and view-model behavior. CI support was improved via a dedicated PR workflow for WinUI/Core tests, and the WinUI project moved toward stricter warning discipline (`warnings as errors`). This area improves both runtime responsiveness and development confidence. - Prevented duplicate operations by ignoring local calendar apply-changes in the wrong paths.
- Added busy-state support and metadata fetch flows so newly created accounts can initialize calendar data more reliably.
### 6) WinUI shell, settings, onboarding, and notification polish ### CalDAV sync
The WinUI shell and app flow were modernized with startup window/onboarding wizard work, settings page refreshes, dialog/title bar improvements, and navigation fixes. Notification behavior was refined with app-entry routing for mail/calendar toasts, redundant-target cleanup, runtime toast dispatch fixes, and tailored image corrections. Additional quality-of-life updates (keyboard shortcuts, global mouse back listener, storage/settings navigability, and startup mode fixes) make daily use more polished and predictable.
--- - Introduced a dedicated CalDAV synchronizer and supporting service/client work.
- Fixed CalDAV delta sync issues.
- Fixed CalDAV timezone issues.
- Added manual live CalDAV workflow tests to validate real-world sync behavior.
## Small changes (commit-by-commit, one-line summary) This means local and self-hosted calendar scenarios are much better represented on this branch than on `main`.
- **44be3eb** — Final settings UI tweaks polished spacing/behavior for the latest shell experience. ### API calendar sync for Outlook and Gmail
- **3e73196** — Removed edit-account-details page to simplify account maintenance flows.
- **8548257** — Corrected update-notes behavior/content for clearer release messaging. - Expanded Outlook calendar sync behavior, including broader sync windows and fixes around date/time handling.
- **d9da326** — Renamed the database artifact to align naming with updated app data conventions. - Improved Gmail drafting and mail/calendar integration so event-related actions work better across providers.
- **d43e2b2** — Fixed tailored notification image handling so visuals render reliably. - Added mail and calendar synchronizer state tracking to make sync progress and error handling more reliable.
- **9d94bad** — Fixed storage page navigation so users can reach storage settings consistently. - Added auto calendar sync on account creation and broader auto-sync trigger and cancellation support.
- **e4a224b** — Added/updated email templates to improve default composition output.
- **15400d4** — Improved keyboard shortcuts for faster power-user navigation and commands. ### Calendar polish and reliability
- **c1568d3** — Added live store update notifications to surface app-update availability.
- **a8f9b2d** — Delivered broad calendar quality improvements across UX and behavior. - Fixed calendar crashes and null-handling issues in calendar view date range updates.
- **1da3408** — Refactored HTML editor toolbar for cleaner structure and easier extensibility. - Fixed double initialization in calendar day views.
- **ebc35c3** — Added event creation capabilities to the calendar workflow. - Improved reaction to calendar changes and calendar item update-source handling.
- **d1f8163** — Refactored web editor internals and improved calendar occurrence summaries. - Added reminder snooze support across toast UI, services, and database storage.
- **09f1cee** — Removed sqlite base64 contact storage from `AccountContact` to modernize data handling.
- **8e8b123** — Updated NuGet dependencies for compatibility, fixes, and maintenance. Overall, Wino Calendar is one of the biggest themes of this branch: richer UI, more complete event workflows, and real sync support across local, CalDAV, Outlook, and Gmail-backed scenarios.
- **9ec7b32** — Merged `feature/EventCompose` into `feature/vNext` to unify event compose work.
- **e94cce4** — Implemented main event compose functionality for calendar authoring. ## Wino Accounts
- **6608bae** — Added initial scaffolding for event composition.
- **5904272** — Added specific handling for Outlook 429 responses to improve throttling resilience. Wino Accounts was significantly expanded and polished on this branch.
- **e1be644** — Delivered contact/settings integration updates for smoother account configuration.
- **51f6446** — Updated core title bar to reflect new menu-item structure. ### Account flows and identity
- **24f7c26** — Refreshed dialog visuals for a more modern WinUI look.
- **1aaf4e8** — Expanded settings UI foundation for broader configuration coverage. - Added sign in, sign out, and registration flows.
- **3d67637** — Consolidated intermediate branch work through merge integration. - Redesigned login and registration dialogs.
- **aaa6e8a** — Removed migrations and introduced wizard-like onboarding steps. - Added privacy policy presentation during registration.
- **db5ecd6** — Added a new startup window to improve initial app entry flow. - Added forgot password and email confirmation flows.
- **d45d3fa** — Implemented “Whats New” surface for post-update feature communication. - Pointed the app to the real API and improved profile caching.
- **5b3739c** — Added snooze support for calendar reminders across UI/service/database layers.
- **e816e87** — Added/expanded contact management capabilities. ### Account management and settings
- **bdd3278** — Reworked folder structure organization for maintainability.
- **f35a433** — Fixed profile-image transparency edge case causing unwanted initials background. - Added Wino account settings and a dedicated management page.
- **2c9351f** — Fixed edge cases in `IsBusy` handling to prevent inconsistent UI state. - Added a special navigation item for Wino Accounts.
- **211faff** — Improved bulk mail operations by using property-change-driven updates. - Added import functionality for Wino Accounts.
- **11158fe** — Removed redundant notification target configuration. - Added a preference to hide the title bar Wino account button.
- **76e3b72** — Fixed issues around mode switching and notification behavior. - Improved the top-shell account icon and signed-out identity visuals.
- **2040d4a** — Optimized mail fetch pipeline with batched DB queries and memory caching.
- **0e742c7** — Resolved warnings and enforced warning-as-error discipline in WinUI. ### Purchases and add-ons
- **d2fce5e** — Added PR GitHub Actions workflow for WinUI build + Core test validation.
- **5c510fd** — Removed single-entry/mode-launch behavior tied to Ctrl key startup. - Added handling for Paddle purchases and add-ons.
- **e1ce856** — Fixed additional startup mode issues for more predictable launches. - Added purchase-success deep linking.
- **4b22608** — Fixed badge creation behavior so badges are consistently generated for Wino Mail. - Added support for AI pack handling through the Microsoft Store.
- **3a39266** — Simplified compose/rendering logic using clearer message-driven flows.
- **5d46ea7** — Routed mail/calendar toasts to correct app entries. ### User-facing polish
- **d51f4a7** — Added SQLite indexes and enforced foreign keys for performance/integrity.
- **79a8171** — Improved thread mapping logic across all synchronizer implementations. - Redesigned the Wino Account flyout and menu with a more polished Fluent-style presentation.
- **c5a631d** — Added grace-period logic for local drafts to reduce accidental loss/conflicts. - Improved account cleanup behavior when an account is deleted.
- **33672ab** — Added local draft resend behavior and default app mode settings. - Added account attention handling and better account details/settings behavior.
- **311b3c7** — Added dedicated Wino Calendar app entry point.
- **17ca32c** — Enabled large Outlook attachment sending via upload sessions. Compared to `main`, this branch turns Wino Accounts into a much more complete platform feature rather than a minimal sign-in surface.
- **9d3f0bd** — Added manual live coverage tests for `ImapSynchronizer`.
- **7f198ba** — Implemented explicit synchronizer state for mail and calendar items. ## Improved Stability and Reliability
- **a912ada** — Fixed messaging issues tied to calendar add/delete operations.
- **317113a** — Fixed CalDAV timezone handling issues. A large part of this branch is about making the app more dependable in everyday use.
- **564cb0b** — Fixed double-initialization issue in calendar day views.
- **ab0810f** — Fixed CalDAV delta sync behavior. ### Synchronization stability
- **7a13ae0** — Added manual live CalDAV workflow tests.
- **c8e1678** — Fixed `HtmlPreviewVisitor` regressions and added sanitization tests. - Refactored synchronizers to address long-standing reliability issues.
- **f49d276** — Added focused ViewModel tests for `WinoMailCollection`. - Improved thread mapping across synchronizers.
- **05112d6** — Ensured WebView2 runtime toast notifications are dispatched on UI thread. - Added generic 404 handling for synchronizers.
- **fec49ce** — Improved UI-side cleanup when deleting an account. - Added specific Outlook 429 handling for rate-limit scenarios.
- **31a7fae** — Added operation-execution error handling in rendering page flow. - Improved Outlook authentication and Outlook sync reliability.
- **dae7d04** — Added calendar metadata fetch after account creation. - Improved Gmail synchronizer behavior.
- **d428a6c** — Ignored local calendar apply-changes in specific paths to prevent duplicates. - Added explicit mail and calendar synchronizer state support.
- **ff25db3** — Added busy-state support to calendar item view models.
- **2baa87d** — Added IMAP local calendar operation tests with in-memory DB. ### Mail and data reliability
- **42e5157** — Landed broad calendar implementation work across multiple components.
- **acf0f64** — Added CalDAV synchronizer and new IMAP setup/edit page. - Optimized mail fetching with batched database queries and in-memory caching.
- **64b9bfc** — Added flag changes to support UID-based IMAP synchronization. - Added SQLite indexes and enabled foreign key enforcement.
- **744145b** — Refactored IMAP synchronization internals for stability. - Switched away from the old mail item queue approach and returned to a simpler initial sync strategy.
- **4a0dcd2** — Removed obsolete project files. - Improved local draft resend behavior and added grace-period handling for local drafts.
- **92df726** — Batched flip-view date-range updates for programmatic calendar navigation. - Added better handling for large Outlook attachments via upload sessions.
- **dbd5812** — Fixed null handling in `WinoCalendarView` date-range updates. - Fixed issues with sent/draft placement, loading mails with infinite scroll, selection cleanup, and deleted-object scenarios.
- **884f000** — Added additional calendar feature plumbing and behaviors.
- **e936c43** — Improved search behavior and relevance/UX. ### UI and lifecycle stability
- **b01fa4e** — Improved event details page and calendar item update source handling.
- **96dcdc8** — Added auto-sync triggering and cancellation support. - Fixed mail rendering page disposal issues.
- **96d2efb** — Removed semantic zoom support to simplify calendar interaction model. - Fixed WebView2 runtime toast dispatching on the UI thread.
- **37199d8** — Fixed cache bug preventing mail removal and improved drag/drop behavior. - Fixed startup mode issues, single-instancing problems, and shell/navigation regressions.
- **52ee5f1** — Added/improved visuals for mail-calendar items and reminders. - Fixed multiple thread selection, container, flicker, and context-menu issues.
- **870a5e2** — Added calendar-to-mail mapping integration. - Fixed crashes and null-reference style issues in several calendar and shell flows.
- **10dd42b** — Fixed thread UI issues for better consistency.
- **0999c71** — Improved contacts UX, thread animations, and image preview controls. ### Engineering quality
- **e559a79** — Added generic 404 handling for synchronizer operations.
- **1747ed8** — Disabled Sentry logging for synchronizer exceptions. - Added more tests across calendar, CalDAV, IMAP, view-model, sanitization, and account sync scenarios.
- **22c6452** — Delivered editor optimizations for better responsiveness. - Added a GitHub Actions workflow to build WinUI and run Core tests on pull requests.
- **ad9b94d** — Removed INC registrations for list view items to reduce overhead. - Resolved warnings and moved the WinUI project toward warnings-as-errors discipline.
- **9f13bcd** — Applied collection-level performance optimizations. - Added AOT compatibility work and related cleanup across the app.
- **5bfa61a** — Added folder create/delete, storage settings, and thread UI adjustments.
- **2cd03d5** — Fixed thread selection issue involving unrealized containers. The branch is not just adding features; it is also clearly reducing failure points throughout sync, rendering, navigation, and storage.
- **c7fb648** — Improved thread selection interactions.
- **331b966** — Added synchronizer info panel in shell for visibility/diagnostics. ## Contacts, Settings, and General UX
- **d28de50** — Fixed Outlook attachments, compose-page reuse, and MIME header details.
- **1ec8d5b** — Added Gmail draft support. This branch also improves the everyday product experience outside mail and calendar core flows.
- **4374d19** — Improved threading behavior and related interaction logic.
- **071f1c9** — Refactored synchronizers broadly to address chronic reliability issues. ### Contacts
- **d1425ca** — Updated Claude permissions ignore configuration.
- **2fd600d** — Added partial busy-state handling for mark-as-read requests. - Added contacts management.
- **0eba778** — Added/updated mail update-source tracking. - Improved contacts UI and related thread/image preview behavior.
- **b343152** — Landed exploratory internal experiments that informed later improvements. - Removed legacy SQLite base64 contact storage from `AccountContact`.
- **31097e4** — Added reactions to calendar changes for better real-time UI updates. - Added contact picture handling support and supporting contact service improvements.
- **319b0af** — Added global mouse back-button listener for app navigation.
- **f105c2f** — Added settings/manage-accounts navigation options. ### Settings
- **7cc201f** — Added `ShowAs` stripe in calendar control template.
- **a23a99c** — Added quick “join online” affordance for meetings. - Added a dedicated settings shell and refactored settings home/navigation.
- **be6b23c** — Made panel usage AOT-safe to improve compatibility. - Expanded settings UI and introduced new setting options.
- Added calendar settings into the settings experience.
- Improved account details/settings pages and storage settings navigation.
- Refined settings visuals, shell integration, and menu behavior.
### Onboarding and app experience
- Added a new startup window and a more guided onboarding flow with wizard-like steps.
- Added a "What's New" implementation for feature communication.
- Improved dialogs, title bar behavior, shell content, navigation, and shell polish across multiple iterations.
- Added live store update notifications.
- Improved keyboard shortcuts and related dialogs.
- Added tray icon support and better toast routing between mail and calendar app entries.
## Summary
Compared to `main`, `feature/vNext` delivers four major leaps:
1. Wino Calendar becomes a substantially more complete feature set, including local calendar support, CalDAV sync, and stronger Outlook and Gmail calendar integration.
2. Wino Accounts becomes a real product surface with better authentication flows, management, imports, purchases, and polish.
3. The app is more stable thanks to synchronization refactors, storage improvements, test expansion, and many crash and lifecycle fixes.
4. Contacts, settings, onboarding, and shell/navigation experience all feel more mature and more consistent.
In short, this branch is a broad product maturation release rather than a narrow feature drop.
@@ -9,22 +9,18 @@ public class ImapClientPoolException : Exception
{ {
} }
public ImapClientPoolException(string message, CustomServerInformation customServerInformation, string protocolLog) : base(message) public ImapClientPoolException(string message, CustomServerInformation customServerInformation) : base(message)
{ {
CustomServerInformation = customServerInformation; CustomServerInformation = customServerInformation;
ProtocolLog = protocolLog;
} }
public ImapClientPoolException(string message, string protocolLog) : base(message) public ImapClientPoolException(string message) : base(message)
{ {
ProtocolLog = protocolLog;
} }
public ImapClientPoolException(Exception innerException, string protocolLog) : base(innerException.Message, innerException) public ImapClientPoolException(Exception innerException) : base(innerException.Message, innerException)
{ {
ProtocolLog = protocolLog;
} }
public CustomServerInformation CustomServerInformation { get; } public CustomServerInformation CustomServerInformation { get; }
public string ProtocolLog { get; }
} }
@@ -1,17 +0,0 @@
using Wino.Core.Domain.Models.AutoDiscovery;
namespace Wino.Core.Domain.Exceptions;
public class ImapConnectionFailedPackage
{
public ImapConnectionFailedPackage(string errorMessage, string protocolLog, AutoDiscoverySettings settings)
{
ErrorMessage = errorMessage;
ProtocolLog = protocolLog;
Settings = settings;
}
public AutoDiscoverySettings Settings { get; }
public string ErrorMessage { get; set; }
public string ProtocolLog { get; }
}
@@ -182,11 +182,6 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary> /// </summary>
MailOperation RightHoverAction { get; set; } MailOperation RightHoverAction { get; set; }
/// <summary>
/// Setting: Whether Mailkit Protocol Logger is enabled for ImapTestService or not.
/// </summary>
bool IsMailkitProtocolLoggerEnabled { get; set; }
/// <summary> /// <summary>
/// Setting: Which entity id (merged account or folder) should be expanded automatically on startup. /// Setting: Which entity id (merged account or folder) should be expanded automatically on startup.
/// </summary> /// </summary>
@@ -1,24 +1,21 @@
using System.IO;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Models.Connectivity; namespace Wino.Core.Domain.Models.Connectivity;
public class ImapClientPoolOptions public class ImapClientPoolOptions
{ {
public Stream ProtocolLog { get; }
public CustomServerInformation ServerInformation { get; } public CustomServerInformation ServerInformation { get; }
public bool IsTestPool { get; } public bool IsTestPool { get; }
protected ImapClientPoolOptions(CustomServerInformation serverInformation, Stream protocolLog, bool isTestPool) protected ImapClientPoolOptions(CustomServerInformation serverInformation, bool isTestPool)
{ {
ServerInformation = serverInformation; ServerInformation = serverInformation;
ProtocolLog = protocolLog;
IsTestPool = isTestPool; IsTestPool = isTestPool;
} }
public static ImapClientPoolOptions CreateDefault(CustomServerInformation serverInformation, Stream protocolLog) public static ImapClientPoolOptions CreateDefault(CustomServerInformation serverInformation)
=> new(serverInformation, protocolLog, false); => new(serverInformation, false);
public static ImapClientPoolOptions CreateTestPool(CustomServerInformation serverInformation, Stream protocolLog) public static ImapClientPoolOptions CreateTestPool(CustomServerInformation serverInformation)
=> new(serverInformation, protocolLog, true); => new(serverInformation, true);
} }
@@ -18,13 +18,10 @@ public class ImapConnectivityTestResults
public bool IsCertificateUIRequired { get; set; } public bool IsCertificateUIRequired { get; set; }
public string FailedReason { get; set; } public string FailedReason { get; set; }
public string FailureProtocolLog { get; set; }
public static ImapConnectivityTestResults Success() => new ImapConnectivityTestResults() { IsSuccess = true }; public static ImapConnectivityTestResults Success() => new ImapConnectivityTestResults() { IsSuccess = true };
public static ImapConnectivityTestResults Failure(Exception ex, string failureProtocolLog) => new ImapConnectivityTestResults() public static ImapConnectivityTestResults Failure(Exception ex) => new ImapConnectivityTestResults()
{ {
FailedReason = string.Join(Environment.NewLine, ex.GetInnerExceptions().Select(e => e.Message)), FailedReason = string.Join(Environment.NewLine, ex.GetInnerExceptions().Select(e => e.Message))
FailureProtocolLog = failureProtocolLog
}; };
public static ImapConnectivityTestResults CertificateUIRequired(string issuer, public static ImapConnectivityTestResults CertificateUIRequired(string issuer,
@@ -664,7 +664,6 @@
"PaneLengthOption_Small": "Small", "PaneLengthOption_Small": "Small",
"Photos": "Photos", "Photos": "Photos",
"PreparingFoldersMessage": "Preparing folders", "PreparingFoldersMessage": "Preparing folders",
"ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.",
"ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_Gmail_Description": "Google Account",
"ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account",
"ProviderDetail_iCloud_Title": "iCloud", "ProviderDetail_iCloud_Title": "iCloud",
+3 -28
View File
@@ -1,12 +1,10 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Security; using System.Net.Security;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -48,7 +46,6 @@ public class ImapClientPool : IDisposable
private readonly ILogger _logger = Log.ForContext<ImapClientPool>(); private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private readonly CustomServerInformation _customServerInformation; private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ConcurrentDictionary<WinoImapClient, ImapClientState> _clientStates = new(); private readonly ConcurrentDictionary<WinoImapClient, ImapClientState> _clientStates = new();
private readonly Channel<WinoImapClient> _availableClients; private readonly Channel<WinoImapClient> _availableClients;
private readonly CancellationTokenSource _maintenanceCts = new(); private readonly CancellationTokenSource _maintenanceCts = new();
@@ -78,7 +75,6 @@ public class ImapClientPool : IDisposable
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions) public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{ {
_customServerInformation = imapClientPoolOptions.ServerInformation; _customServerInformation = imapClientPoolOptions.ServerInformation;
_protocolLogStream = imapClientPoolOptions.ProtocolLog;
ImapClientPoolOptions = imapClientPoolOptions; ImapClientPoolOptions = imapClientPoolOptions;
_quirks = ImapServerQuirks.Resolve(_customServerInformation.IncomingServer); _quirks = ImapServerQuirks.Resolve(_customServerInformation.IncomingServer);
@@ -620,14 +616,7 @@ public class ImapClientPool : IDisposable
private WinoImapClient CreateNewClient() private WinoImapClient CreateNewClient()
{ {
IProtocolLogger protocolLogger = null; var client = new WinoImapClient();
if (_protocolLogStream != null)
{
protocolLogger = new ProtocolLogger(_protocolLogStream, leaveOpen: true);
}
var client = protocolLogger != null ? new WinoImapClient(protocolLogger) : new WinoImapClient();
if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer)) if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer))
{ {
@@ -679,11 +668,9 @@ public class ImapClientPool : IDisposable
private ImapClientPoolException CreatePoolException(string message, Exception innerException = null) private ImapClientPoolException CreatePoolException(string message, Exception innerException = null)
{ {
var protocolLog = GetProtocolLogContent() ?? string.Empty;
return innerException == null return innerException == null
? new ImapClientPoolException(message, _customServerInformation, protocolLog) ? new ImapClientPoolException(message, _customServerInformation)
: new ImapClientPoolException(innerException, protocolLog); : new ImapClientPoolException(innerException);
} }
private static ImapImplementation CreateImplementation() private static ImapImplementation CreateImplementation()
@@ -740,17 +727,6 @@ public class ImapClientPool : IDisposable
return true; return true;
} }
public string GetProtocolLogContent()
{
if (_protocolLogStream == null) return default;
if (_protocolLogStream.CanSeek)
_protocolLogStream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(_protocolLogStream, Encoding.UTF8, true, 1024, leaveOpen: true);
return reader.ReadToEnd();
}
// Legacy compatibility methods // Legacy compatibility methods
public Task<bool> EnsureConnectedAsync(IImapClient client) => public Task<bool> EnsureConnectedAsync(IImapClient client) =>
Task.FromResult(client.IsConnected); Task.FromResult(client.IsConnected);
@@ -784,7 +760,6 @@ public class ImapClientPool : IDisposable
_dedicatedIdleClient = null; _dedicatedIdleClient = null;
} }
_protocolLogStream?.Dispose();
} }
_disposedValue = true; _disposedValue = true;
+1 -6
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Threading; using System.Threading;
using MailKit; using MailKit;
using MailKit.Net.Imap; using MailKit.Net.Imap;
@@ -24,11 +24,6 @@ public class WinoImapClient : ImapClient
HookEvents(); HookEvents();
} }
public WinoImapClient(IProtocolLogger protocolLogger) : base(protocolLogger)
{
HookEvents();
}
private void HookEvents() private void HookEvents()
{ {
Disconnected += ClientDisconnected; Disconnected += ClientDisconnected;
+4 -43
View File
@@ -1,5 +1,3 @@
using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using MailKit.Net.Smtp; using MailKit.Net.Smtp;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
@@ -11,46 +9,18 @@ namespace Wino.Core.Services;
public class ImapTestService : IImapTestService public class ImapTestService : IImapTestService
{ {
public const string ProtocolLogFileName = "ImapProtocolLog.log"; public ImapTestService()
private readonly IPreferencesService _preferencesService;
private readonly IApplicationConfiguration _appInitializerService;
private Stream _protocolLogStream;
public ImapTestService(IPreferencesService preferencesService, IApplicationConfiguration appInitializerService)
{ {
_preferencesService = preferencesService;
_appInitializerService = appInitializerService;
}
private void EnsureProtocolLogFileExists()
{
// Create new file for protocol logger.
var localAppFolderPath = _appInitializerService.ApplicationDataFolderPath;
var logFile = Path.Combine(localAppFolderPath, ProtocolLogFileName);
if (File.Exists(logFile))
File.Delete(logFile);
_protocolLogStream = File.Create(logFile);
} }
public async Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake) public async Task TestImapConnectionAsync(CustomServerInformation serverInformation, bool allowSSLHandShake)
{ {
try var poolOptions = ImapClientPoolOptions.CreateTestPool(serverInformation);
{
EnsureProtocolLogFileExists();
var poolOptions = ImapClientPoolOptions.CreateTestPool(serverInformation, _protocolLogStream); using (var clientPool = new ImapClientPool(poolOptions)
var clientPool = new ImapClientPool(poolOptions)
{ {
ThrowOnSSLHandshakeCallback = !allowSSLHandShake ThrowOnSSLHandshakeCallback = !allowSSLHandShake
}; })
using (clientPool)
{ {
// This call will make sure that everything is authenticated + connected successfully. // This call will make sure that everything is authenticated + connected successfully.
var client = await clientPool.GetClientAsync(); var client = await clientPool.GetClientAsync();
@@ -67,13 +37,4 @@ public class ImapTestService : IImapTestService
if (!smtpClient.IsAuthenticated) if (!smtpClient.IsAuthenticated)
await smtpClient.AuthenticateAsync(serverInformation.OutgoingServerUsername, serverInformation.OutgoingServerPassword); await smtpClient.AuthenticateAsync(serverInformation.OutgoingServerUsername, serverInformation.OutgoingServerPassword);
} }
catch (Exception)
{
throw;
}
finally
{
_protocolLogStream?.Dispose();
}
}
} }
+3 -3
View File
@@ -114,13 +114,13 @@ public class SynchronizationManager : ISynchronizationManager
} }
catch (ImapClientPoolException clientPoolException) catch (ImapClientPoolException clientPoolException)
{ {
_logger.Error(clientPoolException, "IMAP connectivity test failed with protocol log"); _logger.Error(clientPoolException, "IMAP connectivity test failed");
return ImapConnectivityTestResults.Failure(clientPoolException, clientPoolException.ProtocolLog); return ImapConnectivityTestResults.Failure(clientPoolException);
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.Error(exception, "IMAP connectivity test failed"); _logger.Error(exception, "IMAP connectivity test failed");
return ImapConnectivityTestResults.Failure(exception, string.Empty); return ImapConnectivityTestResults.Failure(exception);
} }
} }
+1 -15
View File
@@ -83,7 +83,6 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
IAutoDiscoveryService autoDiscoveryService, IAutoDiscoveryService autoDiscoveryService,
ICalendarService calendarService) : base(account, WeakReferenceMessenger.Default) ICalendarService calendarService) : base(account, WeakReferenceMessenger.Default)
{ {
// Create client pool with account protocol log.
_imapChangeProcessor = imapChangeProcessor; _imapChangeProcessor = imapChangeProcessor;
_applicationConfiguration = applicationConfiguration; _applicationConfiguration = applicationConfiguration;
_unifiedSynchronizer = unifiedSynchronizer; _unifiedSynchronizer = unifiedSynchronizer;
@@ -92,26 +91,13 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
_autoDiscoveryService = autoDiscoveryService; _autoDiscoveryService = autoDiscoveryService;
_calendarService = calendarService; _calendarService = calendarService;
var protocolLogStream = CreateAccountProtocolLogFileStream(); var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation);
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream);
_clientPool = new ImapClientPool(poolOptions); _clientPool = new ImapClientPool(poolOptions);
_localCalendarOperationHandler = new LocalCalendarOperationHandler(Account, _imapChangeProcessor, _calendarService, _applicationConfiguration.ApplicationDataFolderPath, "local"); _localCalendarOperationHandler = new LocalCalendarOperationHandler(Account, _imapChangeProcessor, _calendarService, _applicationConfiguration.ApplicationDataFolderPath, "local");
_calDavCalendarOperationHandler = new CalDavCalendarOperationHandler(this, Account, _calendarService, _calDavClient); _calDavCalendarOperationHandler = new CalDavCalendarOperationHandler(this, Account, _calendarService, _calDavClient);
} }
private Stream CreateAccountProtocolLogFileStream()
{
if (Account == null) throw new ArgumentNullException(nameof(Account));
var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}_{Account.Id}.log");
// Each session should start a new log.
if (File.Exists(logFile)) File.Delete(logFile);
return new FileStream(logFile, FileMode.CreateNew);
}
/// <summary> /// <summary>
/// Returns UniqueId for the given mail copy id. /// Returns UniqueId for the given mail copy id.
/// </summary> /// </summary>
@@ -28,10 +28,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private readonly ICalDavClient _calDavClient; private readonly ICalDavClient _calDavClient;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMailDialogService _mailDialogService; private readonly IMailDialogService _mailDialogService;
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
private readonly WelcomeWizardContext _wizardContext; private readonly WelcomeWizardContext _wizardContext;
private ImapCalDavSettingsPageMode _pageMode; private ImapCalDavSettingsPageMode _pageMode;
private Guid _editingAccountId; private Guid _editingAccountId;
private SpecialImapProvider _editingSpecialImapProvider;
private TaskCompletionSource<ImapCalDavSetupResult> _completionSource; private TaskCompletionSource<ImapCalDavSetupResult> _completionSource;
private bool _isCompletionFinalized; private bool _isCompletionFinalized;
private bool _localOnlyInfoShown; private bool _localOnlyInfoShown;
@@ -261,12 +263,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
ICalDavClient calDavClient, ICalDavClient calDavClient,
IAccountService accountService, IAccountService accountService,
IMailDialogService mailDialogService, IMailDialogService mailDialogService,
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
WelcomeWizardContext wizardContext) WelcomeWizardContext wizardContext)
{ {
_autoDiscoveryService = autoDiscoveryService; _autoDiscoveryService = autoDiscoveryService;
_calDavClient = calDavClient; _calDavClient = calDavClient;
_accountService = accountService; _accountService = accountService;
_mailDialogService = mailDialogService; _mailDialogService = mailDialogService;
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
_wizardContext = wizardContext; _wizardContext = wizardContext;
} }
@@ -368,6 +372,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (!IsCalendarSupportEnabled || SelectedCalendarSupportMode != ImapCalendarSupportMode.CalDav) if (!IsCalendarSupportEnabled || SelectedCalendarSupportMode != ImapCalendarSupportMode.CalDav)
throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavNotRequiredMessage); throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavNotRequiredMessage);
TryApplyKnownProviderSettingsIfNeeded(requireCompleteImapSettings: false, requireCompleteCalDavSettings: true);
var serverInformation = BuildServerInformation(); var serverInformation = BuildServerInformation();
ValidateCalDavSettings(serverInformation); ValidateCalDavSettings(serverInformation);
await ValidateCalDavConnectivityAsync(serverInformation).ConfigureAwait(false); await ValidateCalDavConnectivityAsync(serverInformation).ConfigureAwait(false);
@@ -505,8 +510,10 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (account == null) if (account == null)
throw new InvalidOperationException(Translator.Exception_NullAssignedAccount); throw new InvalidOperationException(Translator.Exception_NullAssignedAccount);
_editingSpecialImapProvider = account.SpecialImapProvider;
DisplayName = account.SenderName ?? string.Empty; DisplayName = account.SenderName ?? string.Empty;
EmailAddress = account.Address ?? string.Empty; EmailAddress = account.Address ?? string.Empty;
ApplyProviderHint(_editingSpecialImapProvider);
ApplyServerInformation(account.ServerInformation); ApplyServerInformation(account.ServerInformation);
@@ -539,10 +546,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav; SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav;
var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None; var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None;
_editingSpecialImapProvider = specialProvider;
ApplyProviderHint(specialProvider);
switch (specialProvider) switch (specialProvider)
{ {
case SpecialImapProvider.iCloud: case SpecialImapProvider.iCloud:
ProviderHint = Translator.ImapCalDavSettingsPage_ICloudHint;
ApplySpecialProviderDefaults( ApplySpecialProviderDefaults(
"imap.mail.me.com", "imap.mail.me.com",
"993", "993",
@@ -556,7 +565,6 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
Password); Password);
break; break;
case SpecialImapProvider.Yahoo: case SpecialImapProvider.Yahoo:
ProviderHint = Translator.ImapCalDavSettingsPage_YahooHint;
ApplySpecialProviderDefaults( ApplySpecialProviderDefaults(
"imap.mail.yahoo.com", "imap.mail.yahoo.com",
"993", "993",
@@ -569,9 +577,6 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
EmailAddress, EmailAddress,
Password); Password);
break; break;
default:
ProviderHint = string.Empty;
break;
} }
} }
@@ -645,6 +650,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (HasCompleteImapSettings()) if (HasCompleteImapSettings())
return; return;
if (TryApplyKnownProviderSettingsIfNeeded(requireCompleteImapSettings: true, requireCompleteCalDavSettings: false))
return;
var minimalSettings = BuildMinimalSettingsOrThrow(); var minimalSettings = BuildMinimalSettingsOrThrow();
await AutoDiscoverAndApplySettingsAsync(minimalSettings).ConfigureAwait(false); await AutoDiscoverAndApplySettingsAsync(minimalSettings).ConfigureAwait(false);
@@ -654,6 +662,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task AutoDiscoverAndApplySettingsAsync(AutoDiscoveryMinimalSettings minimalSettings) private async Task AutoDiscoverAndApplySettingsAsync(AutoDiscoveryMinimalSettings minimalSettings)
{ {
if (TryApplyKnownProviderSettings(alwaysApplyForKnownProvider: true))
return;
var discoverySettings = await _autoDiscoveryService.GetAutoDiscoverySettings(minimalSettings).ConfigureAwait(false); var discoverySettings = await _autoDiscoveryService.GetAutoDiscoverySettings(minimalSettings).ConfigureAwait(false);
if (discoverySettings == null) if (discoverySettings == null)
@@ -930,6 +941,96 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (string.IsNullOrWhiteSpace(serverInformation.CalDavPassword)) if (string.IsNullOrWhiteSpace(serverInformation.CalDavPassword))
throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavPasswordRequired); throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavPasswordRequired);
} }
private void ApplyProviderHint(SpecialImapProvider provider)
{
ProviderHint = provider switch
{
SpecialImapProvider.iCloud => Translator.ImapCalDavSettingsPage_ICloudHint,
SpecialImapProvider.Yahoo => Translator.ImapCalDavSettingsPage_YahooHint,
_ => string.Empty
};
}
private bool TryApplyKnownProviderSettingsIfNeeded(bool requireCompleteImapSettings, bool requireCompleteCalDavSettings)
{
var needsImapSettings = requireCompleteImapSettings && !HasCompleteImapSettings();
var needsCalDavSettings = requireCompleteCalDavSettings
&& IsCalendarSupportEnabled
&& SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav
&& !HasCompleteCalDavSettings();
if (!needsImapSettings && !needsCalDavSettings)
return false;
return TryApplyKnownProviderSettings(alwaysApplyForKnownProvider: false);
}
private bool TryApplyKnownProviderSettings(bool alwaysApplyForKnownProvider)
{
if (_editingSpecialImapProvider is not (SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo))
return false;
var effectivePassword = GetKnownProviderPasswordCandidate();
if (string.IsNullOrWhiteSpace(EmailAddress) || string.IsNullOrWhiteSpace(effectivePassword))
return false;
if (!alwaysApplyForKnownProvider && HasCompleteImapSettings() && HasCompleteCalDavSettings())
return false;
var mode = IsCalendarSupportEnabled ? SelectedCalendarSupportMode : ImapCalendarSupportMode.Disabled;
var providerDetails = new SpecialImapProviderDetails(
EmailAddress.Trim(),
effectivePassword,
DisplayName.Trim(),
_editingSpecialImapProvider,
mode);
var serverInformation = _specialImapProviderConfigResolver.GetServerInformation(
new MailAccount
{
Address = EmailAddress.Trim(),
SenderName = DisplayName.Trim(),
ProviderType = MailProviderType.IMAP4,
SpecialImapProvider = _editingSpecialImapProvider,
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
},
new AccountCreationDialogResult(MailProviderType.IMAP4, DisplayName.Trim(), providerDetails, string.Empty));
if (serverInformation == null)
return false;
serverInformation.ProxyServer = (ProxyServer ?? string.Empty).Trim();
serverInformation.ProxyServerPort = (ProxyServerPort ?? string.Empty).Trim();
serverInformation.MaxConcurrentClients = MaxConcurrentClients <= 0 ? serverInformation.MaxConcurrentClients : MaxConcurrentClients;
ApplyServerInformation(serverInformation);
Password = effectivePassword;
return true;
}
private string GetKnownProviderPasswordCandidate()
{
if (!string.IsNullOrWhiteSpace(Password))
return Password;
if (!string.IsNullOrWhiteSpace(IncomingServerPassword))
return IncomingServerPassword;
if (!string.IsNullOrWhiteSpace(OutgoingServerPassword))
return OutgoingServerPassword;
return CalDavPassword ?? string.Empty;
}
private bool HasCompleteCalDavSettings()
=> !IsCalendarSupportEnabled
|| SelectedCalendarSupportMode != ImapCalendarSupportMode.CalDav
|| (!string.IsNullOrWhiteSpace(CalDavServiceUrl)
&& Uri.TryCreate(CalDavServiceUrl, UriKind.Absolute, out _)
&& !string.IsNullOrWhiteSpace(CalDavUsername)
&& !string.IsNullOrWhiteSpace(CalDavPassword));
private bool HasCompleteImapSettings() private bool HasCompleteImapSettings()
=> !string.IsNullOrWhiteSpace(IncomingServer) => !string.IsNullOrWhiteSpace(IncomingServer)
&& !string.IsNullOrWhiteSpace(IncomingServerPort) && !string.IsNullOrWhiteSpace(IncomingServerPort)
+1
View File
@@ -231,6 +231,7 @@ public partial class App : WinoApplication,
if (windowManager.GetWindow(WinoWindowKind.Welcome) is not WelcomeWindow welcomeWindow) if (windowManager.GetWindow(WinoWindowKind.Welcome) is not WelcomeWindow welcomeWindow)
return; return;
welcomeWindow.PrepareForClose();
welcomeWindow.AllowClose(); welcomeWindow.AllowClose();
welcomeWindow.Close(); welcomeWindow.Close();
} }
@@ -1,35 +1,35 @@
{ {
"sections": [ "sections": [
{ {
"title": "# Wino Calendar is here!", "title": "Wino Calendar is here!",
"description": "You can now create local or remote CalDAV-compatible calendars, manage recurring events, and respond to invitations — all from within Wino.", "description": "You can now create local or remote CalDAV-compatible calendars, manage recurring events, and respond to invitations — all from within Wino.",
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg", "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Calendar.svg",
"imageWidth": 128, "imageWidth": 128,
"imageHeight": 128 "imageHeight": 128
}, },
{ {
"title": "# S/MIME Signing & Encryption", "title": "S/MIME Signing & Encryption",
"description": "Wino now supports signing and encrypting your emails with personal certificates. Keep your communications secure and verifiable.", "description": "Wino now supports signing and encrypting your emails with personal certificates. Keep your communications secure and verifiable.",
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg", "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Security.svg",
"imageWidth": 128, "imageWidth": 128,
"imageHeight": 128 "imageHeight": 128
}, },
{ {
"title": "# Threaded Mail View", "title": "Threaded Mail View",
"description": "Emails are now grouped by conversation, making it easier to follow long discussions without losing context.", "description": "Emails are now grouped by conversation, making it easier to follow long discussions without losing context.",
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg", "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Thread.svg",
"imageWidth": 128, "imageWidth": 128,
"imageHeight": 128 "imageHeight": 128
}, },
{ {
"title": "# Smarter Notifications", "title": "Smarter Notifications",
"description": "Act on your emails directly from toast notifications — mark as read, delete, or archive without opening the app.", "description": "Act on your emails directly from toast notifications — mark as read, delete, or archive without opening the app.",
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg", "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/Notification.svg",
"imageWidth": 128, "imageWidth": 128,
"imageHeight": 128 "imageHeight": 128
}, },
{ {
"title": "# And much more...", "title": "And much more...",
"description": "Folder management, swipe actions, keyboard shortcuts, a custom print dialog, and significant performance improvements are all included in this release.\n\nThank you for using Wino Mail!", "description": "Folder management, swipe actions, keyboard shortcuts, a custom print dialog, and significant performance improvements are all included in this release.\n\nThank you for using Wino Mail!",
"imageUrl": "ms-appx:///Assets/UpdateNotes/Images/More.svg", "imageUrl": "ms-appx:///Assets/UpdateNotes/Images/More.svg",
"imageWidth": 128, "imageWidth": 128,
+36 -4
View File
@@ -13,6 +13,8 @@ namespace Wino.Mail.WinUI;
public partial class BasePage : Page, IRecipient<LanguageChanged> public partial class BasePage : Page, IRecipient<LanguageChanged>
{ {
private bool _isPreparedForClose;
public void Receive(LanguageChanged message) public void Receive(LanguageChanged message)
{ {
OnLanguageChanged(); OnLanguageChanged();
@@ -31,6 +33,24 @@ public partial class BasePage : Page, IRecipient<LanguageChanged>
protected virtual void UnregisterRecipients() { } protected virtual void UnregisterRecipients() { }
public virtual CoreBaseViewModel? AssociatedViewModel => null; public virtual CoreBaseViewModel? AssociatedViewModel => null;
public virtual void PrepareForClose()
{
if (_isPreparedForClose)
return;
_isPreparedForClose = true;
WeakReferenceMessenger.Default.Unregister<LanguageChanged>(this);
UnregisterRecipients();
}
protected void ResetPreparedForCloseState()
{
_isPreparedForClose = false;
}
protected bool IsPreparedForClose => _isPreparedForClose;
} }
public abstract class BasePage<T> : BasePage where T : CoreBaseViewModel public abstract class BasePage<T> : BasePage where T : CoreBaseViewModel
@@ -63,6 +83,7 @@ public abstract class BasePage<T> : BasePage where T : CoreBaseViewModel
var mode = GetNavigationMode(e.NavigationMode); var mode = GetNavigationMode(e.NavigationMode);
var parameter = e.Parameter; var parameter = e.Parameter;
ResetPreparedForCloseState();
WeakReferenceMessenger.Default.Register<LanguageChanged>(this); WeakReferenceMessenger.Default.Register<LanguageChanged>(this);
RegisterRecipients(); RegisterRecipients();
@@ -76,14 +97,25 @@ public abstract class BasePage<T> : BasePage where T : CoreBaseViewModel
var mode = GetNavigationMode(e.NavigationMode); var mode = GetNavigationMode(e.NavigationMode);
var parameter = e.Parameter; var parameter = e.Parameter;
WeakReferenceMessenger.Default.Unregister<LanguageChanged>(this); PrepareForClose(mode, parameter);
UnregisterRecipients();
ViewModel.OnNavigatedFrom(mode, parameter);
GC.Collect(); GC.Collect();
} }
public override void PrepareForClose()
{
PrepareForClose(WinoNavigationMode.New, null);
}
private void PrepareForClose(WinoNavigationMode mode, object? parameter)
{
if (IsPreparedForClose)
return;
base.PrepareForClose();
ViewModel.OnNavigatedFrom(mode, parameter!);
}
private WinoNavigationMode GetNavigationMode(NavigationMode mode) private WinoNavigationMode GetNavigationMode(NavigationMode mode)
{ {
return (WinoNavigationMode)mode; return (WinoNavigationMode)mode;
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Wino.Mail.WinUI.Helpers;
internal static class WindowCleanupHelper
{
public static void CleanupFrame(Frame? frame)
{
if (frame == null)
return;
CleanupObject(frame.Content);
frame.BackStack.Clear();
frame.ForwardStack.Clear();
frame.Content = null;
}
public static void CleanupObject(object? instance)
{
if (instance == null)
return;
var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
CleanupObject(instance, visited);
}
private static void CleanupObject(object? instance, HashSet<object> visited)
{
if (instance == null || !visited.Add(instance))
return;
switch (instance)
{
case Views.WinoAppShell shell:
shell.PrepareForWindowClose();
break;
case Frame frame:
CleanupFrame(frame);
break;
case BasePage page:
page.PrepareForClose();
break;
}
if (instance is DependencyObject dependencyObject)
{
var childCount = VisualTreeHelper.GetChildrenCount(dependencyObject);
for (int i = 0; i < childCount; i++)
{
CleanupObject(VisualTreeHelper.GetChild(dependencyObject, i), visited);
}
}
if (instance is IDisposable disposable)
{
disposable.Dispose();
}
}
private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
public static ReferenceEqualityComparer Instance { get; } = new();
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
@@ -221,12 +221,6 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(IsLoggingEnabled), value); set => SetPropertyAndSave(nameof(IsLoggingEnabled), value);
} }
public bool IsMailkitProtocolLoggerEnabled
{
get => _configurationService.Get(nameof(IsMailkitProtocolLoggerEnabled), false);
set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value);
}
public bool IsGravatarEnabled public bool IsGravatarEnabled
{ {
get => _configurationService.Get(nameof(IsGravatarEnabled), true); get => _configurationService.Get(nameof(IsGravatarEnabled), true);
+10
View File
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Models.Synchronization;
using Wino.Extensions; using Wino.Extensions;
using Wino.Mail.WinUI.Activation; using Wino.Mail.WinUI.Activation;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Mail.WinUI.Helpers;
using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models; using Wino.Mail.WinUI.Models;
using Wino.Mail.WinUI.Views; using Wino.Mail.WinUI.Views;
@@ -46,6 +47,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
private ITitleBarSearchHost? _activeTitleBarSearchHost; private ITitleBarSearchHost? _activeTitleBarSearchHost;
private bool _isBackButtonVisibilityReady; private bool _isBackButtonVisibilityReady;
private bool _isSynchronizingTitleBarSearch; private bool _isSynchronizingTitleBarSearch;
private bool _isPreparedForClose;
public ShellWindow() public ShellWindow()
{ {
@@ -365,11 +367,18 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
public void PrepareForClose() public void PrepareForClose()
{ {
if (_isPreparedForClose)
return;
_isPreparedForClose = true;
if (MainShellFrame.Content is WinoAppShell shellPage) if (MainShellFrame.Content is WinoAppShell shellPage)
{ {
shellPage.PrepareForWindowClose(); shellPage.PrepareForWindowClose();
} }
WindowCleanupHelper.CleanupFrame(MainShellFrame);
_allowClose = true; _allowClose = true;
} }
@@ -384,6 +393,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
shellPage.PrepareForWindowClose(); shellPage.PrepareForWindowClose();
} }
WindowCleanupHelper.CleanupFrame(MainShellFrame);
UnregisterRecipients(); UnregisterRecipients();
} }
@@ -25,19 +25,21 @@ public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
{ {
base.OnNavigatedTo(e); base.OnNavigatedTo(e);
WeakReferenceMessenger.Default.Register<BreadcrumbNavigationRequested>(this);
WeakReferenceMessenger.Default.Register<BackBreadcrumNavigationRequested>(this);
ResetWizard(); ResetWizard();
} }
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) protected override void RegisterRecipients()
{ {
base.RegisterRecipients();
WeakReferenceMessenger.Default.Register<BreadcrumbNavigationRequested>(this);
WeakReferenceMessenger.Default.Register<BackBreadcrumNavigationRequested>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
WeakReferenceMessenger.Default.Unregister<BreadcrumbNavigationRequested>(this); WeakReferenceMessenger.Default.Unregister<BreadcrumbNavigationRequested>(this);
WeakReferenceMessenger.Default.Unregister<BackBreadcrumNavigationRequested>(this); WeakReferenceMessenger.Default.Unregister<BackBreadcrumNavigationRequested>(this);
base.OnNavigatingFrom(e);
} }
public void Receive(BreadcrumbNavigationRequested message) public void Receive(BreadcrumbNavigationRequested message)
@@ -28,6 +28,7 @@ using Wino.Mail.ViewModels;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.ViewModels; using Wino.Mail.WinUI.ViewModels;
using Wino.Mail.WinUI.Controls; using Wino.Mail.WinUI.Controls;
using Wino.Mail.WinUI.Helpers;
using Wino.MenuFlyouts; using Wino.MenuFlyouts;
using Wino.MenuFlyouts.Context; using Wino.MenuFlyouts.Context;
using Wino.Messaging.Client.Accounts; using Wino.Messaging.Client.Accounts;
@@ -127,6 +128,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
navigationView.MenuItemsSource = null; navigationView.MenuItemsSource = null;
CalendarHostListView.ItemsSource = null; CalendarHostListView.ItemsSource = null;
WindowCleanupHelper.CleanupFrame(InnerShellFrame);
} }
private void OnLoaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
+19
View File
@@ -3,6 +3,7 @@ using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Mail.WinUI.Helpers;
using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Interfaces;
using WinUIEx; using WinUIEx;
@@ -11,6 +12,7 @@ namespace Wino.Mail.WinUI;
public sealed partial class WelcomeWindow : WindowEx public sealed partial class WelcomeWindow : WindowEx
{ {
private bool _allowClose; private bool _allowClose;
private bool _isPreparedForClose;
public Frame GetRootFrame() => RootFrame; public Frame GetRootFrame() => RootFrame;
@@ -25,6 +27,7 @@ public sealed partial class WelcomeWindow : WindowEx
ConfigureWindowChrome(); ConfigureWindowChrome();
AppWindow.Closing += OnAppWindowClosing; AppWindow.Closing += OnAppWindowClosing;
Closed += OnWindowClosed;
} }
private void ConfigureWindowChrome() private void ConfigureWindowChrome()
@@ -55,4 +58,20 @@ public sealed partial class WelcomeWindow : WindowEx
{ {
_allowClose = true; _allowClose = true;
} }
public void PrepareForClose()
{
if (_isPreparedForClose)
return;
_isPreparedForClose = true;
WindowCleanupHelper.CleanupFrame(RootFrame);
}
private void OnWindowClosed(object sender, WindowEventArgs e)
{
Closed -= OnWindowClosed;
AppWindow.Closing -= OnAppWindowClosing;
PrepareForClose();
}
} }