Partial Busy state for mark as read requests
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replacing the deprecated Windows Mail & Calendar. It's **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI), never edit the deprecated Wino.Mail UWP project.
|
||||||
|
|
||||||
|
## Build and Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open solution
|
||||||
|
# WinoMail.slnx is the main solution file (VS 2022+)
|
||||||
|
|
||||||
|
# Build from command line
|
||||||
|
dotnet build WinoMail.slnx -c Debug
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj
|
||||||
|
|
||||||
|
# Build specific platform
|
||||||
|
dotnet build WinoMail.slnx -c Debug /p:Platform=x64
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
|
||||||
|
|
||||||
|
**Startup project:** Wino.Mail.WinUI
|
||||||
|
|
||||||
|
**Platforms:** x86, x64, ARM64
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Solution Structure
|
||||||
|
```
|
||||||
|
Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
|
||||||
|
Wino.Core → Synchronization engine, authenticators, request processing
|
||||||
|
Wino.Services → Database, mail, folder, account services
|
||||||
|
Wino.Authentication → OAuth2 authenticators (Outlook, Gmail)
|
||||||
|
Wino.Mail.ViewModels → Mail-specific ViewModels
|
||||||
|
Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
|
||||||
|
Wino.Messaging → Pub-sub message definitions
|
||||||
|
Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
|
||||||
|
Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mail Synchronization Flow
|
||||||
|
1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
|
||||||
|
2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
|
||||||
|
3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
|
||||||
|
4. **ChangeProcessors** → Apply changes to local SQLite database
|
||||||
|
5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
|
||||||
|
|
||||||
|
### Synchronizer Types
|
||||||
|
- **OutlookSynchronizer** - Microsoft Graph SDK for Office 365
|
||||||
|
- **GmailSynchronizer** - Gmail API
|
||||||
|
- **ImapSynchronizer** - MimeKit/MailKit for IMAP/SMTP
|
||||||
|
|
||||||
|
### Queue-Based Sync Pattern
|
||||||
|
- Initial sync queues mail IDs first (MailItemQueue table), downloads metadata only
|
||||||
|
- MIME content downloaded on-demand when user opens mail
|
||||||
|
- Check `MailItemFolder.IsInitialSyncCompleted` for sync state
|
||||||
|
- See QUEUE_SYNC_IMPLEMENTATION.md for details
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
|
||||||
|
- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
|
||||||
|
- ViewModels registered in App.xaml.cs
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### MVVM with Source Generators
|
||||||
|
**CORRECT - use public partial properties:**
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string SearchQuery { get; set; } = string.Empty;
|
||||||
|
```
|
||||||
|
|
||||||
|
**WRONG - will not work:**
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty]
|
||||||
|
private string searchQuery = string.Empty;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Messenger Pattern
|
||||||
|
- ViewModels inherit from CoreBaseViewModel or MailBaseViewModel
|
||||||
|
- Register handlers in `RegisterRecipients()`, unregister in `UnregisterRecipients()`
|
||||||
|
- Send via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
|
||||||
|
|
||||||
|
### Data Binding - No Converters
|
||||||
|
- **NEVER** create IValueConverter classes
|
||||||
|
- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
|
||||||
|
- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}`
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json`
|
||||||
|
2. Build project - source generators create Translator properties
|
||||||
|
3. Use `Translator.{PropertyName}` in code/XAML
|
||||||
|
4. **NEVER** edit other language files - Crowdin manages translations
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
- **SQLite database** in publisher cache folder (shared with future Wino Calendar)
|
||||||
|
- **EML files** in app local storage, referenced by `MailCopy.FileId`
|
||||||
|
- Paths resolved via `MimeFileService.GetMimeMessagePath()`
|
||||||
|
|
||||||
|
## WebView2 Mail Rendering
|
||||||
|
|
||||||
|
- `reader.html` for reading mails, `editor.html` for composing (Jodit editor)
|
||||||
|
- Virtual host mapping: `https://wino.mail/reader.html`
|
||||||
|
- JavaScript interop via `ExecuteScriptFunctionAsync()`
|
||||||
|
- MIME content downloaded on-demand, not during sync
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Forgetting to register ViewModels in App.xaml.cs `RegisterViewModels()`
|
||||||
|
- Not calling `RegisterRecipients()` for message handlers
|
||||||
|
- Using private fields with `[ObservableProperty]` instead of public partial
|
||||||
|
- Creating IValueConverter classes instead of using XamlHelpers
|
||||||
|
- Editing UWP project files instead of WinUI equivalents
|
||||||
|
- Hardcoding strings instead of using Translator
|
||||||
|
- Forgetting to unregister Messenger recipients (memory leaks)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Avoid introducing new NuGet packages when possible
|
||||||
|
- Use existing libraries (MimeKit, MailKit, Microsoft Graph, Gmail API)
|
||||||
|
- Use `var` where type is obvious
|
||||||
|
- String interpolation over string.Format
|
||||||
|
- Wrap async operations in try-catch
|
||||||
|
- Log errors via IWinoLogger
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
# Outlook Queue-Based Synchronization Implementation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes the implementation of the queue-based, metadata-only synchronization system for Outlook, mirroring the approach used in Gmail but adapted for Outlook's per-folder synchronization model.
|
|
||||||
|
|
||||||
## Key Differences from Gmail
|
|
||||||
|
|
||||||
1. **Per-Folder Queue**: Unlike Gmail which uses per-account `InitialSynchronizationStatus`, Outlook uses per-folder queue tracking via `RemoteFolderId` in `MailItemQueue`.
|
|
||||||
2. **Folder-Level Processing**: Each folder maintains its own delta token and processes its queue independently.
|
|
||||||
3. **No Account-Level Status**: Outlook doesn't use `Account.InitialSynchronizationStatus` since sync is per-folder, not per-account.
|
|
||||||
|
|
||||||
## Architecture Changes
|
|
||||||
|
|
||||||
### 1. MailItemQueue Entity Enhancement
|
|
||||||
|
|
||||||
**File**: `Wino.Core.Domain\Entities\Mail\MailItemQueue.cs`
|
|
||||||
|
|
||||||
Added `RemoteFolderId` property to support per-folder queue tracking:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public string RemoteFolderId { get; set; } // For Outlook per-folder sync
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Service Layer Updates
|
|
||||||
|
|
||||||
**Files**:
|
|
||||||
- `Wino.Core.Domain\Interfaces\IMailService.cs`
|
|
||||||
- `Wino.Services\MailService.cs`
|
|
||||||
- `Wino.Core\Integration\Processors\DefaultChangeProcessor.cs`
|
|
||||||
|
|
||||||
Added new methods for folder-specific queue operations:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
Task<List<MailItemQueue>> GetMailItemQueueByFolderAsync(Guid accountId, string remoteFolderId, int take);
|
|
||||||
Task<int> GetMailItemQueueCountByFolderAsync(Guid accountId, string remoteFolderId);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. OutlookSynchronizer Redesign
|
|
||||||
|
|
||||||
**File**: `Wino.Core\Synchronizers\OutlookSynchronizer.cs`
|
|
||||||
|
|
||||||
#### New Methods
|
|
||||||
|
|
||||||
1. **QueueMailIdsForFolderAsync**
|
|
||||||
- Queues all mail IDs for a specific folder using Delta API
|
|
||||||
- Only retrieves message IDs (minimal data transfer)
|
|
||||||
- Stores delta token for future incremental syncs
|
|
||||||
- Creates queue entries with `RemoteFolderId` for folder tracking
|
|
||||||
|
|
||||||
2. **ProcessMailQueueForFolderAsync**
|
|
||||||
- Processes queued mail IDs in batches
|
|
||||||
- Downloads metadata only (no MIME content)
|
|
||||||
- Handles failures with retry logic
|
|
||||||
- Updates queue item status (IsProcessed, FailedCount)
|
|
||||||
|
|
||||||
3. **DownloadMessageMetadataBatchAsync**
|
|
||||||
- Downloads metadata for a batch of messages concurrently
|
|
||||||
- Uses semaphore to limit concurrent downloads (10 max)
|
|
||||||
- Calls `CreateMailCopyFromMessage` for metadata extraction
|
|
||||||
- Creates `NewMailItemPackage` with null MimeMessage
|
|
||||||
|
|
||||||
4. **CreateMailCopyFromMessage** _(Centralized)_
|
|
||||||
- **REPLACES** scattered `AsMailCopy()` and `CreateMinimalMailCopyAsync()` calls
|
|
||||||
- Single source of truth for converting Graph Message to MailCopy
|
|
||||||
- Extracts all required fields from metadata
|
|
||||||
- Sets FolderId, UniqueId, and FileId
|
|
||||||
|
|
||||||
#### Modified Methods
|
|
||||||
|
|
||||||
1. **DownloadMailsForInitialSyncAsync**
|
|
||||||
- Now orchestrates queue-based sync
|
|
||||||
- Step 1: Queue all mail IDs via Delta API
|
|
||||||
- Step 2: Process queue in batches
|
|
||||||
|
|
||||||
2. **ProcessDeltaChangesAndDownloadMailsAsync**
|
|
||||||
- Downloads delta changes with metadata only
|
|
||||||
- Uses `DownloadMessageMetadataBatchAsync` instead of full MIME download
|
|
||||||
|
|
||||||
3. **CreateNewMailPackagesAsync**
|
|
||||||
- Still downloads MIME for specific scenarios (search results, drafts)
|
|
||||||
- Uses `CreateMailCopyFromMessage` for consistency
|
|
||||||
- Not called during normal sync operations
|
|
||||||
|
|
||||||
#### Removed Methods
|
|
||||||
|
|
||||||
- `DownloadMailsConcurrentlyAsync` - Replaced by queue system
|
|
||||||
- `DownloadSingleMailAsync` - Replaced by queue batch processing
|
|
||||||
- Scattered `CreateMinimalMailCopyAsync` implementations
|
|
||||||
|
|
||||||
## Synchronization Flow
|
|
||||||
|
|
||||||
### Initial Sync (Per Folder)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. SynchronizeFolderAsync
|
|
||||||
├─ Check: !folder.IsInitialSyncCompleted
|
|
||||||
└─ DownloadMailsForInitialSyncAsync
|
|
||||||
├─ QueueMailIdsForFolderAsync
|
|
||||||
│ ├─ Use Delta API with Select=["Id"]
|
|
||||||
│ ├─ Iterate all pages
|
|
||||||
│ ├─ Create MailItemQueue entries with RemoteFolderId
|
|
||||||
│ └─ Store delta token
|
|
||||||
└─ ProcessMailQueueForFolderAsync
|
|
||||||
├─ Get queue items by folder (100 at a time)
|
|
||||||
├─ Process in chunks of 20
|
|
||||||
├─ DownloadMessageMetadataBatchAsync
|
|
||||||
│ ├─ Concurrent download (10 max)
|
|
||||||
│ ├─ GetMessageByIdAsync (metadata fields only)
|
|
||||||
│ ├─ CreateMailCopyFromMessage
|
|
||||||
│ └─ CreateMailAsync (package with null MIME)
|
|
||||||
└─ Update queue status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Delta Sync (Per Folder)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. SynchronizeFolderAsync
|
|
||||||
├─ Check: folder.IsInitialSyncCompleted
|
|
||||||
└─ ProcessDeltaChangesAndDownloadMailsAsync
|
|
||||||
├─ Use Delta API with existing token
|
|
||||||
├─ Collect new mail IDs
|
|
||||||
├─ DownloadMessageMetadataBatchAsync
|
|
||||||
│ ├─ Download metadata only
|
|
||||||
│ └─ Create MailCopy entries
|
|
||||||
└─ Update delta token
|
|
||||||
```
|
|
||||||
|
|
||||||
### On-Demand MIME Download
|
|
||||||
|
|
||||||
```
|
|
||||||
User Reads Mail
|
|
||||||
└─ DownloadMissingMimeMessageAsync
|
|
||||||
├─ Download full MIME via /messages/{id}/$value
|
|
||||||
└─ SaveMimeFileAsync
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Reduced Bandwidth**: Only metadata downloaded during sync (no 50+ MB MIME files)
|
|
||||||
2. **Faster Sync**: Parallel processing with controlled concurrency
|
|
||||||
3. **Resilient**: Queue system handles failures gracefully with retry logic
|
|
||||||
4. **Consistent**: Centralized `CreateMailCopyFromMessage` method
|
|
||||||
5. **Scalable**: Per-folder processing allows independent folder syncs
|
|
||||||
|
|
||||||
## Code Consolidation
|
|
||||||
|
|
||||||
### Before (Scattered Approach)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Multiple places creating MailCopy
|
|
||||||
var mailCopy = message.AsMailCopy();
|
|
||||||
mailCopy.FolderId = folder.Id;
|
|
||||||
// ... repeat in 3+ locations
|
|
||||||
|
|
||||||
// Mixed MIME downloading
|
|
||||||
var package = await CreateNewMailPackagesAsync(...); // Downloads MIME
|
|
||||||
var minimal = await CreateMinimalMailCopyAsync(...); // No MIME
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (Centralized Approach)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Single method for all scenarios
|
|
||||||
var mailCopy = CreateMailCopyFromMessage(message, folder);
|
|
||||||
|
|
||||||
// Clear separation
|
|
||||||
// Sync: metadata only
|
|
||||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
|
|
||||||
|
|
||||||
// On-demand: full MIME
|
|
||||||
await DownloadMissingMimeMessageAsync(mailCopy, ...);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Graph API Fields Used
|
|
||||||
|
|
||||||
Only essential fields are requested during sync:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
private readonly string[] outlookMessageSelectParameters =
|
|
||||||
[
|
|
||||||
"InferenceClassification",
|
|
||||||
"Flag",
|
|
||||||
"Importance",
|
|
||||||
"IsRead",
|
|
||||||
"IsDraft",
|
|
||||||
"ReceivedDateTime",
|
|
||||||
"HasAttachments",
|
|
||||||
"BodyPreview",
|
|
||||||
"Id",
|
|
||||||
"ConversationId",
|
|
||||||
"From",
|
|
||||||
"Subject",
|
|
||||||
"ParentFolderId",
|
|
||||||
"InternetMessageId",
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOT downloaded during sync**:
|
|
||||||
- Body content (HTML/Text)
|
|
||||||
- Raw MIME message
|
|
||||||
- Attachment content
|
|
||||||
- Extended properties
|
|
||||||
|
|
||||||
## Migration Path for IMAP (Future)
|
|
||||||
|
|
||||||
The base `WinoSynchronizer` class has been updated with:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
protected virtual Task QueueMailIdsForInitialSyncAsync(MailItemFolder folder, CancellationToken cancellationToken = default);
|
|
||||||
protected virtual Task<List<string>> DownloadMailsFromQueueAsync(MailItemFolder folder, int batchSize, CancellationToken cancellationToken = default);
|
|
||||||
protected virtual Task<MailCopy> CreateMinimalMailCopyAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
|
|
||||||
```
|
|
||||||
|
|
||||||
IMAP can override these methods to implement similar queue-based sync:
|
|
||||||
- TODO: Add folder-based queue support to IMAP
|
|
||||||
- TODO: Implement metadata-only header parsing
|
|
||||||
- TODO: Centralize IMAP MailCopy creation
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
- [x] Initial sync queues all mail IDs per folder
|
|
||||||
- [x] Queue processing downloads metadata only
|
|
||||||
- [x] Delta sync uses metadata-only approach
|
|
||||||
- [x] Failed queue items retry correctly
|
|
||||||
- [x] Concurrent download respects semaphore limits
|
|
||||||
- [x] Delta token stored and used correctly per folder
|
|
||||||
- [ ] Search results still download MIME when needed
|
|
||||||
- [ ] Draft handling works with MIME headers
|
|
||||||
- [ ] On-demand MIME download functions correctly
|
|
||||||
- [ ] Large folders (1000+ messages) sync efficiently
|
|
||||||
- [ ] Network interruption recovery
|
|
||||||
|
|
||||||
## Performance Expectations
|
|
||||||
|
|
||||||
### Before (With MIME):
|
|
||||||
- 100 messages ≈ 100-500 MB download
|
|
||||||
- Sync time: 5-10 minutes
|
|
||||||
- API calls: 100+ individual message downloads
|
|
||||||
|
|
||||||
### After (Metadata Only):
|
|
||||||
- 100 messages ≈ 1-5 MB download (metadata)
|
|
||||||
- Sync time: 30-60 seconds
|
|
||||||
- API calls: Batched requests (10-20 concurrent)
|
|
||||||
- MIME downloaded only when user reads (lazy loading)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- `CreateNewMailPackagesAsync` is now marked as **only for special cases**
|
|
||||||
- `DefaultChangeProcessor` no longer needed for basic operations
|
|
||||||
- All synchronizers can benefit from this pattern (Gmail, IMAP, Outlook)
|
|
||||||
- The `InitialSyncMimeDownloadCount` property is now obsolete
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# Mail Synchronization Queue-Based Implementation
|
|
||||||
|
|
||||||
This document summarizes the changes made to implement the new queue-based mail synchronization system for Wino Mail.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The new system changes the mail synchronization approach from downloading everything immediately during initial sync to queuing mail IDs first and then downloading mail content progressively. This makes initial synchronization much more efficient and responsive.
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. New Database Entity: MailItemQueue
|
|
||||||
|
|
||||||
**File:** `Wino.Core.Domain/Entities/Mail/MailItemQueue.cs`
|
|
||||||
|
|
||||||
Created a new table to store mail IDs that need to be downloaded from the server:
|
|
||||||
- `Id`: Primary key (auto-increment)
|
|
||||||
- `AccountId`: Account that owns the mail
|
|
||||||
- `FolderId`: Local folder ID
|
|
||||||
- `RemoteFolderId`: Server-specific folder ID
|
|
||||||
- `MailCopyId`: Mail ID from the remote server
|
|
||||||
- `QueuedDate`: When the item was queued
|
|
||||||
- `Priority`: Priority for processing (lower number = higher priority)
|
|
||||||
|
|
||||||
### 2. Enhanced MailItemFolder Entity
|
|
||||||
|
|
||||||
**File:** `Wino.Core.Domain/Entities/Mail/MailItemFolder.cs`
|
|
||||||
|
|
||||||
Added new property:
|
|
||||||
- `IsInitialSyncCompleted`: Boolean flag to track whether initial mail ID synchronization is complete for the folder
|
|
||||||
|
|
||||||
### 3. New Queue Management Service
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `Wino.Core.Domain/Interfaces/IMailItemQueueService.cs`
|
|
||||||
- `Wino.Services/MailItemQueueService.cs`
|
|
||||||
|
|
||||||
Created a comprehensive service for managing the mail queue with methods to:
|
|
||||||
- Queue mail items for download
|
|
||||||
- Get next batch of items to process
|
|
||||||
- Remove processed items from queue
|
|
||||||
- Check queue counts and existence
|
|
||||||
- Clear queue for folders/accounts
|
|
||||||
|
|
||||||
### 4. Updated Database Service
|
|
||||||
|
|
||||||
**File:** `Wino.Services/DatabaseService.cs`
|
|
||||||
|
|
||||||
Added `MailItemQueue` to the database table creation list.
|
|
||||||
|
|
||||||
### 5. Enhanced Base Synchronizer
|
|
||||||
|
|
||||||
**File:** `Wino.Core/Synchronizers/WinoSynchronizer.cs`
|
|
||||||
|
|
||||||
Added new virtual methods that synchronizers can override to support queue-based sync:
|
|
||||||
- `QueueMailIdsForInitialSyncAsync()`: Queue all mail IDs for initial sync
|
|
||||||
- `DownloadMailsFromQueueAsync()`: Download mails from queue in batches
|
|
||||||
- `CreateMinimalMailCopyAsync()`: Create MailCopy with minimal properties (no MIME download)
|
|
||||||
|
|
||||||
### 6. OutlookSynchronizer Implementation
|
|
||||||
|
|
||||||
**File:** `Wino.Core/Synchronizers/OutlookSynchronizer.cs`
|
|
||||||
|
|
||||||
Major changes to implement the new synchronization logic:
|
|
||||||
|
|
||||||
#### Constructor Changes
|
|
||||||
- Added `IMailItemQueueService` dependency injection
|
|
||||||
|
|
||||||
#### New Synchronization Algorithm
|
|
||||||
The `SynchronizeFolderAsync` method now implements the new algorithm:
|
|
||||||
|
|
||||||
1. **Check Initial Sync Status**: If `IsInitialSyncCompleted` is false:
|
|
||||||
- Clear existing queue items for the folder
|
|
||||||
- Queue all mail IDs using `QueueMailIdsForInitialSyncAsync()`
|
|
||||||
- Mark initial sync as completed
|
|
||||||
|
|
||||||
2. **Process Queue**: Download mails from queue in batches (50 at a time):
|
|
||||||
- Get queued items for the folder
|
|
||||||
- Download each mail with minimal properties (no MIME)
|
|
||||||
- Create MailCopy objects with essential fields only
|
|
||||||
- Remove processed items from queue
|
|
||||||
|
|
||||||
3. **Process Delta Changes**: Handle incremental changes using existing delta sync logic
|
|
||||||
|
|
||||||
#### New Methods Implemented
|
|
||||||
- `QueueMailIdsForInitialSyncAsync()`: Uses PageIterator to efficiently get all message IDs
|
|
||||||
- `CreateMinimalMailCopyAsync()`: Creates MailCopy without downloading MIME content
|
|
||||||
- `GetMessageByIdAsync()`: Downloads individual messages with selected properties only
|
|
||||||
- `ProcessDeltaChangesAsync()`: Handles incremental sync with delta tokens
|
|
||||||
|
|
||||||
### 7. Enhanced Change Processor Interface
|
|
||||||
|
|
||||||
**File:** `Wino.Core/Integration/Processors/DefaultChangeProcessor.cs`
|
|
||||||
|
|
||||||
Added new method to `IOutlookChangeProcessor`:
|
|
||||||
- `UpdateFolderInitialSyncCompletedAsync()`: Updates the initial sync completion status
|
|
||||||
|
|
||||||
**File:** `Wino.Core/Integration/Processors/OutlookChangeProcessor.cs`
|
|
||||||
|
|
||||||
Implemented the new method to update the `IsInitialSyncCompleted` field in the database.
|
|
||||||
|
|
||||||
### 8. Dependency Injection Updates
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `Wino.Core/Services/SynchronizerFactory.cs`: Added `IMailItemQueueService` dependency and updated OutlookSynchronizer creation
|
|
||||||
- `Wino.Services/ServicesContainerSetup.cs`: Registered `IMailItemQueueService` as transient service
|
|
||||||
|
|
||||||
## Key Benefits
|
|
||||||
|
|
||||||
1. **Faster Initial Sync**: Only mail IDs are downloaded initially, making the sync much faster
|
|
||||||
2. **Progressive Loading**: Mail content is downloaded progressively based on queue
|
|
||||||
3. **Better User Experience**: Users see folder structure and mail list faster
|
|
||||||
4. **Efficient Resource Usage**: Avoids downloading full MIME messages during initial sync
|
|
||||||
5. **Prioritization Support**: Queue system supports priority-based processing
|
|
||||||
6. **Resilient**: Can handle sync interruptions and resume from where it left off
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Queue-Based Processing
|
|
||||||
- Initial sync: Download only message IDs and queue them
|
|
||||||
- Progressive download: Process queue items in batches
|
|
||||||
- Minimal properties: Download only essential mail properties (Subject, Preview, etc.)
|
|
||||||
- No MIME download: Full MIME messages are not downloaded during initial sync
|
|
||||||
|
|
||||||
### Delta Sync Integration
|
|
||||||
- Existing delta sync logic is preserved for incremental changes
|
|
||||||
- Delta tokens are properly managed and updated
|
|
||||||
- Expired tokens trigger queue reset and re-sync
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Robust error handling for individual mail downloads
|
|
||||||
- Failed items don't block the entire batch
|
|
||||||
- Proper logging for debugging and monitoring
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
The architecture is designed to be easily extended to other synchronizers:
|
|
||||||
- Gmail and IMAP synchronizers can adopt the same pattern
|
|
||||||
- Common functionality is in the base `WinoSynchronizer` class
|
|
||||||
- Queue service is provider-agnostic
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
1. Test initial synchronization with large mailboxes
|
|
||||||
2. Verify progressive loading of mail content
|
|
||||||
3. Test interruption and resume scenarios
|
|
||||||
4. Validate delta sync functionality
|
|
||||||
5. Test with multiple accounts and folders
|
|
||||||
6. Verify queue management operations
|
|
||||||
7. Test error scenarios and recovery
|
|
||||||
|
|
||||||
This implementation significantly improves the initial synchronization experience while maintaining all existing functionality for incremental syncing.
|
|
||||||
@@ -6,9 +6,14 @@ namespace Wino.Core.Domain.Enums;
|
|||||||
public enum MailUpdateSource
|
public enum MailUpdateSource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update originated from client-side UI changes (ApplyUIChanges/RevertUIChanges).
|
/// Update originated from client-side UI changes (ApplyUIChanges).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Client,
|
ClientUpdated,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from client-side UI revert (RevertUIChanges).
|
||||||
|
/// </summary>
|
||||||
|
ClientReverted,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update originated from server synchronization or database operations.
|
/// Update originated from server synchronization or database operations.
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> Mail
|
|||||||
{
|
{
|
||||||
foreach (var item in MailsToMarkRead)
|
foreach (var item in MailsToMarkRead)
|
||||||
{
|
{
|
||||||
|
// Skip if already read
|
||||||
|
if (item.IsRead) continue;
|
||||||
|
|
||||||
item.IsRead = true;
|
item.IsRead = true;
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.Client));
|
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,9 +28,12 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> Mail
|
|||||||
{
|
{
|
||||||
foreach (var item in MailsToMarkRead)
|
foreach (var item in MailsToMarkRead)
|
||||||
{
|
{
|
||||||
|
// Skip if already unread (wasn't changed by ApplyUIChanges)
|
||||||
|
if (!item.IsRead) continue;
|
||||||
|
|
||||||
item.IsRead = false;
|
item.IsRead = false;
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.Client));
|
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, MailUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,18 +18,30 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
|
|||||||
|
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag;
|
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeFlag;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this request represents an actual state change.
|
||||||
|
/// If the mail is already in the desired flagged state, no change is needed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsNoOp => Item.IsFlagged == IsFlagged;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
{
|
{
|
||||||
|
// Skip UI update if the mail is already in the desired state
|
||||||
|
if (IsNoOp) return;
|
||||||
|
|
||||||
Item.IsFlagged = IsFlagged;
|
Item.IsFlagged = IsFlagged;
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.Client));
|
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RevertUIChanges()
|
public override void RevertUIChanges()
|
||||||
{
|
{
|
||||||
|
// Skip UI revert if this was a no-op request
|
||||||
|
if (IsNoOp) return;
|
||||||
|
|
||||||
Item.IsFlagged = !IsFlagged;
|
Item.IsFlagged = !IsFlagged;
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.Client));
|
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,18 +17,30 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
|
|||||||
|
|
||||||
public bool ExcludeMustHaveFolders => true;
|
public bool ExcludeMustHaveFolders => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this request represents an actual state change.
|
||||||
|
/// If the mail is already in the desired read state, no change is needed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsNoOp => Item.IsRead == IsRead;
|
||||||
|
|
||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
{
|
{
|
||||||
|
// Skip UI update if the mail is already in the desired state
|
||||||
|
if (IsNoOp) return;
|
||||||
|
|
||||||
Item.IsRead = IsRead;
|
Item.IsRead = IsRead;
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.Client));
|
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RevertUIChanges()
|
public override void RevertUIChanges()
|
||||||
{
|
{
|
||||||
|
// Skip UI revert if this was a no-op request
|
||||||
|
if (IsNoOp) return;
|
||||||
|
|
||||||
Item.IsRead = !IsRead;
|
Item.IsRead = !IsRead;
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.Client));
|
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task UpdateMailCopy(MailCopy updatedMailCopy)
|
public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource)
|
||||||
{
|
{
|
||||||
// This item doesn't exist in the list.
|
// This item doesn't exist in the list.
|
||||||
if (!MailCopyIdHashSet.ContainsKey(updatedMailCopy.UniqueId)) return Task.CompletedTask;
|
if (!MailCopyIdHashSet.ContainsKey(updatedMailCopy.UniqueId)) return Task.CompletedTask;
|
||||||
@@ -743,14 +743,14 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
|||||||
// This maintains reference integrity and ensures PropertyChanged is raised for all properties
|
// This maintains reference integrity and ensures PropertyChanged is raised for all properties
|
||||||
itemContainer.ItemViewModel.UpdateFrom(updatedMailCopy);
|
itemContainer.ItemViewModel.UpdateFrom(updatedMailCopy);
|
||||||
|
|
||||||
|
// Mark the item view model as busy until the network operation is completed.
|
||||||
|
itemContainer.ItemViewModel.IsBusy = mailUpdateSource == MailUpdateSource.ClientUpdated;
|
||||||
|
|
||||||
UpdateUniqueIdHashes(itemContainer.ItemViewModel, true);
|
UpdateUniqueIdHashes(itemContainer.ItemViewModel, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thread property notifications if this item is in a thread
|
// Trigger thread property notifications if this item is in a thread
|
||||||
if (itemContainer.ThreadViewModel != null)
|
itemContainer.ThreadViewModel?.NotifyMailItemUpdated(itemContainer.ItemViewModel);
|
||||||
{
|
|
||||||
itemContainer.ThreadViewModel.NotifyMailItemUpdated(itemContainer.ItemViewModel);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
|
|||||||
[NotifyPropertyChangedRecipients]
|
[NotifyPropertyChangedRecipients]
|
||||||
public partial bool IsSelected { get; set; }
|
public partial bool IsSelected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if this mail item is currently being processed by a network operation.
|
||||||
|
/// Used to show loading state in the UI.
|
||||||
|
/// </summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
||||||
public DateTime CreationDate
|
public DateTime CreationDate
|
||||||
{
|
{
|
||||||
get => MailCopy.CreationDate;
|
get => MailCopy.CreationDate;
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
|
|||||||
[NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))]
|
[NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))]
|
||||||
public partial bool IsSelected { get; set; }
|
public partial bool IsSelected { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
||||||
public bool IsSelectedOrExpanded => IsSelected || IsThreadExpanded;
|
public bool IsSelectedOrExpanded => IsSelected || IsThreadExpanded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -668,7 +668,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
{
|
{
|
||||||
base.OnMailUpdated(updatedMail, source);
|
base.OnMailUpdated(updatedMail, source);
|
||||||
|
|
||||||
await MailCollection.UpdateMailCopy(updatedMail);
|
await MailCollection.UpdateMailCopy(updatedMail, source);
|
||||||
|
|
||||||
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,13 @@
|
|||||||
x:Name="IsFlaggedContent"
|
x:Name="IsFlaggedContent"
|
||||||
x:Load="{x:Bind IsFlagged, Mode=OneWay}"
|
x:Load="{x:Bind IsFlagged, Mode=OneWay}"
|
||||||
ContentTemplate="{StaticResource FlaggedSymbolControlTemplate}" />
|
ContentTemplate="{StaticResource FlaggedSymbolControlTemplate}" />
|
||||||
|
|
||||||
|
<ProgressRing
|
||||||
|
Width="3"
|
||||||
|
Height="3"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
IsActive="{x:Bind IsBusy, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Windows.Input;
|
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.UI.Composition;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Hosting;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
@@ -21,6 +22,11 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
|||||||
|
|
||||||
public bool IsRunningHoverAction { get; set; }
|
public bool IsRunningHoverAction { get; set; }
|
||||||
|
|
||||||
|
// Busy animation fields
|
||||||
|
private Compositor? _compositor;
|
||||||
|
private Visual? _contentVisual;
|
||||||
|
private ScalarKeyFrameAnimation? _opacityAnimation;
|
||||||
|
|
||||||
[GeneratedDependencyProperty(DefaultValue = MailListDisplayMode.Spacious)]
|
[GeneratedDependencyProperty(DefaultValue = MailListDisplayMode.Spacious)]
|
||||||
public partial MailListDisplayMode DisplayMode { get; set; }
|
public partial MailListDisplayMode DisplayMode { get; set; }
|
||||||
|
|
||||||
@@ -45,6 +51,9 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
|||||||
[GeneratedDependencyProperty(DefaultValue = true)]
|
[GeneratedDependencyProperty(DefaultValue = true)]
|
||||||
public partial bool IsHoverActionsEnabled { get; set; }
|
public partial bool IsHoverActionsEnabled { get; set; }
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty(DefaultValue = false)]
|
||||||
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
||||||
public event EventHandler<MailOperationPreperationRequest>? HoverActionExecuted;
|
public event EventHandler<MailOperationPreperationRequest>? HoverActionExecuted;
|
||||||
|
|
||||||
[GeneratedDependencyProperty(DefaultValue = false)]
|
[GeneratedDependencyProperty(DefaultValue = false)]
|
||||||
@@ -124,6 +133,55 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
|||||||
IconsContainer.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
|
IconsContainer.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
|
||||||
|
|
||||||
RootContainerVisualWrapper.SizeChanged += (s, e) => leftBackgroundVisual.Size = e.NewSize.ToVector2();
|
RootContainerVisualWrapper.SizeChanged += (s, e) => leftBackgroundVisual.Size = e.NewSize.ToVector2();
|
||||||
|
|
||||||
|
// Initialize shimmer effect compositor
|
||||||
|
_compositor = this.Visual().Compositor;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsBusyChanged(bool newValue)
|
||||||
|
{
|
||||||
|
if (newValue)
|
||||||
|
{
|
||||||
|
StartBusyAnimation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StopBusyAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartBusyAnimation()
|
||||||
|
{
|
||||||
|
if (_compositor == null) return;
|
||||||
|
|
||||||
|
// Get the visual for the content area
|
||||||
|
_contentVisual = ElementCompositionPreview.GetElementVisual(MainContentContainer);
|
||||||
|
|
||||||
|
// Create a subtle opacity pulse animation (1.0 -> 0.4 -> 1.0)
|
||||||
|
_opacityAnimation = _compositor.CreateScalarKeyFrameAnimation();
|
||||||
|
_opacityAnimation.InsertKeyFrame(0f, 1f);
|
||||||
|
_opacityAnimation.InsertKeyFrame(0.5f, 0.4f, _compositor.CreateCubicBezierEasingFunction(new Vector2(0.42f, 0f), new Vector2(0.58f, 1f)));
|
||||||
|
_opacityAnimation.InsertKeyFrame(1f, 1f, _compositor.CreateCubicBezierEasingFunction(new Vector2(0.42f, 0f), new Vector2(0.58f, 1f)));
|
||||||
|
_opacityAnimation.Duration = TimeSpan.FromSeconds(1.0);
|
||||||
|
_opacityAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||||
|
|
||||||
|
// Start animation
|
||||||
|
_contentVisual.StartAnimation("Opacity", _opacityAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopBusyAnimation()
|
||||||
|
{
|
||||||
|
if (_contentVisual != null)
|
||||||
|
{
|
||||||
|
_contentVisual.StopAnimation("Opacity");
|
||||||
|
|
||||||
|
// Reset to default value
|
||||||
|
_contentVisual.Opacity = 1f;
|
||||||
|
|
||||||
|
_contentVisual = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_opacityAnimation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsFlaggedChanged(bool newValue)
|
partial void OnIsFlaggedChanged(bool newValue)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
FromName="{x:Bind FromName}"
|
FromName="{x:Bind FromName}"
|
||||||
HasAttachments="{x:Bind HasAttachments, Mode=OneWay}"
|
HasAttachments="{x:Bind HasAttachments, Mode=OneWay}"
|
||||||
HoverActionExecuted="MailItemDisplayInformationControl_HoverActionExecuted"
|
HoverActionExecuted="MailItemDisplayInformationControl_HoverActionExecuted"
|
||||||
|
IsBusy="{x:Bind IsBusy, Mode=OneWay}"
|
||||||
IsDraft="{x:Bind IsDraft, Mode=OneWay}"
|
IsDraft="{x:Bind IsDraft, Mode=OneWay}"
|
||||||
IsFlagged="{x:Bind IsFlagged, Mode=OneWay}"
|
IsFlagged="{x:Bind IsFlagged, Mode=OneWay}"
|
||||||
IsRead="{x:Bind IsRead, Mode=OneWay}"
|
IsRead="{x:Bind IsRead, Mode=OneWay}"
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
FromName="{x:Bind FromName, Mode=OneWay}"
|
FromName="{x:Bind FromName, Mode=OneWay}"
|
||||||
HasAttachments="{x:Bind HasAttachments, Mode=OneWay}"
|
HasAttachments="{x:Bind HasAttachments, Mode=OneWay}"
|
||||||
HoverActionExecuted="MailItemDisplayInformationControl_HoverActionExecuted"
|
HoverActionExecuted="MailItemDisplayInformationControl_HoverActionExecuted"
|
||||||
|
IsBusy="{x:Bind IsBusy, Mode=OneWay}"
|
||||||
IsDraft="{x:Bind IsDraft, Mode=OneWay}"
|
IsDraft="{x:Bind IsDraft, Mode=OneWay}"
|
||||||
IsFlagged="{x:Bind IsFlagged, Mode=OneWay}"
|
IsFlagged="{x:Bind IsFlagged, Mode=OneWay}"
|
||||||
IsRead="{x:Bind IsRead, Mode=OneWay}"
|
IsRead="{x:Bind IsRead, Mode=OneWay}"
|
||||||
@@ -208,13 +210,14 @@
|
|||||||
<Border
|
<Border
|
||||||
x:Name="MailListContainer"
|
x:Name="MailListContainer"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
|
Margin="2"
|
||||||
Padding="5,5,5,0"
|
Padding="5,5,5,0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Background="{ThemeResource WinoContentZoneBackgroud}"
|
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="{ThemeResource OverlayCornerRadius}"
|
CornerRadius="{ThemeResource OverlayCornerRadius}"
|
||||||
Translation="0,0,10">
|
Translation="0,0,50">
|
||||||
<Border.Shadow>
|
<Border.Shadow>
|
||||||
<ThemeShadow />
|
<ThemeShadow />
|
||||||
</Border.Shadow>
|
</Border.Shadow>
|
||||||
|
|||||||
@@ -169,7 +169,11 @@
|
|||||||
Background="{ThemeResource WinoContentZoneBackgroud}"
|
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="{ThemeResource OverlayCornerRadius}">
|
CornerRadius="{ThemeResource OverlayCornerRadius}"
|
||||||
|
Translation="0,0,20">
|
||||||
|
<Border.Shadow>
|
||||||
|
<ThemeShadow />
|
||||||
|
</Border.Shadow>
|
||||||
<Grid Margin="8">
|
<Grid Margin="8">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -473,6 +477,7 @@
|
|||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="{ThemeResource OverlayCornerRadius}">
|
CornerRadius="{ThemeResource OverlayCornerRadius}">
|
||||||
<Grid Margin="1" CornerRadius="{ThemeResource OverlayCornerRadius}">
|
<Grid Margin="1" CornerRadius="{ThemeResource OverlayCornerRadius}">
|
||||||
|
|
||||||
<Grid Background="White" Visibility="{x:Bind IsDarkEditor, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />
|
<Grid Background="White" Visibility="{x:Bind IsDarkEditor, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />
|
||||||
|
|
||||||
<WebView2 x:Name="Chromium" NavigationStarting="WebViewNavigationStarting">
|
<WebView2 x:Name="Chromium" NavigationStarting="WebViewNavigationStarting">
|
||||||
|
|||||||
Reference in New Issue
Block a user