General account details settings and some marking mail issues
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
---
|
||||
description: 'WinUI 3 and Windows App SDK coding guidelines. Prevents common UWP API misuse, enforces correct XAML namespaces, threading, windowing, and MVVM patterns for desktop Windows apps.'
|
||||
applyTo: '**/*.xaml, **/*.cs, **/*.csproj'
|
||||
---
|
||||
|
||||
# WinUI 3 / Windows App SDK
|
||||
|
||||
## Critical Rules — NEVER Use Legacy UWP APIs
|
||||
|
||||
These UWP patterns are **wrong** for WinUI 3 desktop apps. Always use the Windows App SDK equivalent.
|
||||
|
||||
- **NEVER** use `Windows.UI.Popups.MessageDialog`. Use `ContentDialog` with `XamlRoot` set.
|
||||
- **NEVER** show a `ContentDialog` without setting `dialog.XamlRoot = this.Content.XamlRoot` first.
|
||||
- **NEVER** use `CoreDispatcher.RunAsync` or `Dispatcher.RunAsync`. Use `DispatcherQueue.TryEnqueue`.
|
||||
- **NEVER** use `Window.Current`. Track the main window via a static `App.MainWindow` property.
|
||||
- **NEVER** use `Windows.UI.Xaml.*` namespaces. Use `Microsoft.UI.Xaml.*`.
|
||||
- **NEVER** use `Windows.UI.Composition`. Use `Microsoft.UI.Composition`.
|
||||
- **NEVER** use `Windows.UI.Colors`. Use `Microsoft.UI.Colors`.
|
||||
- **NEVER** use `ApplicationView` or `CoreWindow` for window management. Use `Microsoft.UI.Windowing.AppWindow`.
|
||||
- **NEVER** use `CoreApplicationViewTitleBar`. Use `AppWindowTitleBar`.
|
||||
- **NEVER** use `GetForCurrentView()` patterns (e.g., `UIViewSettings.GetForCurrentView()`). These do not exist in desktop WinUI 3. Use `AppWindow` APIs instead.
|
||||
- **NEVER** use UWP `PrintManager` directly. Use `IPrintManagerInterop` with a window handle.
|
||||
- **NEVER** use `DataTransferManager` directly for sharing. Use `IDataTransferManagerInterop` with a window handle.
|
||||
- **NEVER** use UWP `IBackgroundTask`. Use `Microsoft.Windows.AppLifecycle` activation.
|
||||
- **NEVER** use `WebAuthenticationBroker`. Use `OAuth2Manager` (Windows App SDK 1.7+).
|
||||
|
||||
## XAML Patterns
|
||||
|
||||
- The default XAML namespace maps to `Microsoft.UI.Xaml`, not `Windows.UI.Xaml`.
|
||||
- Prefer `{x:Bind}` over `{Binding}` for compiled, type-safe, higher-performance bindings.
|
||||
- Set `x:DataType` on `DataTemplate` elements when using `{x:Bind}` — this is required for compiled bindings in templates. On Page/UserControl, `x:DataType` enables compile-time binding validation but is not strictly required if the DataContext does not change.
|
||||
- Use `Mode=OneWay` for dynamic values, `Mode=OneTime` for static, `Mode=TwoWay` only for editable inputs.
|
||||
- Do not bind static constants — set them directly in XAML.
|
||||
|
||||
## Threading
|
||||
|
||||
- Use `DispatcherQueue.TryEnqueue(() => { ... })` to update UI from background threads.
|
||||
- `TryEnqueue` returns `bool`, not a `Task` — it is fire-and-forget.
|
||||
- Check thread access with `DispatcherQueue.HasThreadAccess` before dispatching.
|
||||
- WinUI 3 uses standard STA (not ASTA). No built-in reentrancy protection — be cautious with async code that pumps messages.
|
||||
|
||||
## Windowing
|
||||
|
||||
- Get the `AppWindow` from a WinUI 3 `Window` via `WindowNative.GetWindowHandle` → `Win32Interop.GetWindowIdFromWindow` → `AppWindow.GetFromWindowId`.
|
||||
- Use `AppWindow` for resize, move, title, and presenter operations.
|
||||
- Custom title bar: use `AppWindow.TitleBar` properties, not `CoreApplicationViewTitleBar`.
|
||||
- Track the main window as `App.MainWindow` (a static property set in `OnLaunched`).
|
||||
|
||||
## Dialogs and Pickers
|
||||
|
||||
- **ContentDialog**: Always set `dialog.XamlRoot = this.Content.XamlRoot` before calling `ShowAsync()`.
|
||||
- **File/Folder Pickers**: Initialize with `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` where `hwnd` comes from `WindowNative.GetWindowHandle(App.MainWindow)`.
|
||||
- **Share/Print**: Use COM interop interfaces (`IDataTransferManagerInterop`, `IPrintManagerInterop`) with window handles.
|
||||
|
||||
## MVVM and Data Binding
|
||||
|
||||
- Prefer `CommunityToolkit.Mvvm` (`[ObservableProperty]`, `[RelayCommand]`) for MVVM infrastructure.
|
||||
- Use `Microsoft.Extensions.DependencyInjection` for service registration and injection.
|
||||
- Keep UI (Views) focused on layout and bindings; keep logic in ViewModels and services.
|
||||
- Use `async`/`await` for I/O and long-running work to keep the UI responsive.
|
||||
|
||||
## Project Setup
|
||||
|
||||
- Target `net10.0-windows10.0.22621.0` (or appropriate TFM for the project's target SDK).
|
||||
- Set `<UseWinUI>true</UseWinUI>` in the project file.
|
||||
- Reference the latest stable `Microsoft.WindowsAppSDK` NuGet package.
|
||||
- Use `System.Text.Json` with source generators for JSON serialization.
|
||||
|
||||
## C# Code Style
|
||||
|
||||
- Use file-scoped namespaces.
|
||||
- Enable nullable reference types. Use `is null` / `is not null` instead of `== null`.
|
||||
- Prefer pattern matching over `as`/`is` with null checks.
|
||||
- PascalCase for types, methods, properties. camelCase for private fields.
|
||||
- Allman brace style (opening brace on its own line).
|
||||
- Prefer explicit types for built-in types; use `var` only when the type is obvious.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Set `AutomationProperties.Name` on all interactive controls.
|
||||
- Use `AutomationProperties.HeadingLevel` on section headers.
|
||||
- Hide decorative elements with `AutomationProperties.AccessibilityView="Raw"`.
|
||||
- Ensure full keyboard navigation (Tab, Enter, Space, arrow keys).
|
||||
- Meet WCAG color contrast requirements.
|
||||
|
||||
## Performance
|
||||
|
||||
- Prefer `{x:Bind}` (compiled) over `{Binding}` (reflection-based).
|
||||
- **NativeAOT:** Under Native AOT compilation, `{Binding}` (reflection-based) does not work at all. Only `{x:Bind}` (compiled bindings) is supported. If the project uses NativeAOT, use `{x:Bind}` exclusively.
|
||||
- Use `x:Load` or `x:DeferLoadStrategy` for UI elements that are not immediately needed.
|
||||
- Use `ItemsRepeater` with virtualization for large lists.
|
||||
- Avoid deep layout nesting — prefer `Grid` over nested `StackPanel` chains.
|
||||
- Use `async`/`await` for all I/O; never block the UI thread.
|
||||
|
||||
## App Settings (Packaged vs Unpackaged)
|
||||
|
||||
- **Packaged apps**: `ApplicationData.Current.LocalSettings` works as expected.
|
||||
- **Unpackaged apps**: Use a custom settings file (e.g., JSON in `Environment.GetFolderPath(SpecialFolder.LocalApplicationData)`).
|
||||
- Do not assume `ApplicationData` is always available — check packaging status first.
|
||||
|
||||
## Typography
|
||||
|
||||
- **Always** use built-in TextBlock styles (`CaptionTextBlockStyle`, `BodyTextBlockStyle`, `BodyStrongTextBlockStyle`, `SubtitleTextBlockStyle`, `TitleTextBlockStyle`, `TitleLargeTextBlockStyle`, `DisplayTextBlockStyle`).
|
||||
- Prefer using the built-in TextBlock styles over hardcoding `FontSize`, `FontWeight`, or `FontFamily`.
|
||||
- Font: Segoe UI Variable is the default — do not change it.
|
||||
- Use sentence casing for all UI text.
|
||||
|
||||
|
||||
## Theming & Colors
|
||||
|
||||
- **Always** use `{ThemeResource}` for brushes and colors to support Light, Dark, and High Contrast themes automatically.
|
||||
- **Never** hardcode color values (`#FFFFFF`, `Colors.White`, etc.) for UI elements. Use theme resources like `TextFillColorPrimaryBrush`, `CardBackgroundFillColorDefaultBrush`, `CardStrokeColorDefaultBrush`.
|
||||
- Use `SystemAccentColor` (and `Light1`–`Light3`, `Dark1`–`Dark3` variants) for the user's accent color palette.
|
||||
- For borders: use `CardStrokeColorDefaultBrush` or `ControlStrokeColorDefaultBrush`.
|
||||
|
||||
## Spacing & Layout
|
||||
|
||||
- Use a **4px grid system**: all margins, padding, and spacing values must be multiples of 4px.
|
||||
- Standard spacing: 4 (compact), 8 (controls), 12 (small gutters), 16 (content padding), 24 (large gutters).
|
||||
- Prefer `Grid` over deeply nested `StackPanel` chains for performance.
|
||||
- Use `Auto` for content-sized rows/columns, `*` for proportional sizing. Avoid fixed pixel sizes.
|
||||
- Use `VisualStateManager` with `AdaptiveTrigger` for responsive layouts at breakpoints (640px, 1008px).
|
||||
- Use `ControlCornerRadius` (4px) for small controls and `OverlayCornerRadius` (8px) for cards, dialogs, flyouts.
|
||||
|
||||
## Materials & Elevation
|
||||
|
||||
- Use **Mica** (`MicaBackdrop`) for the app window backdrop. Requires transparent layers above to show through.
|
||||
- Use **Acrylic** for transient surfaces only (flyouts, menus, navigation panes).
|
||||
- Use `LayerFillColorDefaultBrush` for content layers above Mica.
|
||||
- Use `ThemeShadow` with Z-axis `Translation` for elevation. Cards: 4–8 px, Flyouts: 32 px, Dialogs: 128 px.
|
||||
|
||||
## Motion & Transitions
|
||||
|
||||
- Use built-in theme transitions (`EntranceThemeTransition`, `RepositionThemeTransition`, `ContentThemeTransition`, `AddDeleteThemeTransition`).
|
||||
- Avoid custom storyboard animations when a built-in transition exists.
|
||||
|
||||
## Control Selection
|
||||
|
||||
- Use `NavigationView` for primary app navigation (not custom sidebars).
|
||||
- Use `InfoBar` for persistent in-app notifications (not custom banners).
|
||||
- Use `TeachingTip` for contextual guidance (not custom popups).
|
||||
- Use `NumberBox` for numeric input (not TextBox with manual validation).
|
||||
- Use `ToggleSwitch` for on/off settings (not CheckBox).
|
||||
- Use `ItemsView` as the modern collection control for displaying data with built-in selection, virtualization, and layout flexibility.
|
||||
- Use `ListView`/`GridView` for standard virtualized lists and grids, especially when built-in selection support is needed.
|
||||
- Use `ItemsRepeater` only for fully custom virtualizing layouts where you need complete control over rendering and do not need built-in selection or interaction handling.
|
||||
- Use `Expander` for collapsible sections (not custom visibility toggling).
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Always wrap `async void` event handlers in try/catch to prevent unhandled crashes.
|
||||
- Use `InfoBar` (with `Severity = Error`) for user-facing error messages, not `ContentDialog` for routine errors.
|
||||
- Handle `App.UnhandledException` for logging and graceful recovery.
|
||||
|
||||
## Testing
|
||||
|
||||
- **NEVER** use a plain MSTest or xUnit project for tests that instantiate WinUI 3 XAML types. Use a **Unit Test App (WinUI in Desktop)** project, which provides the Xaml runtime and UI thread.
|
||||
- Use `[TestMethod]` for pure logic tests. Use `[UITestMethod]` for any test that creates or interacts with `Microsoft.UI.Xaml` types (controls, pages, user controls).
|
||||
- Place testable business logic in a **Class Library (WinUI in Desktop)** project, separate from the main app.
|
||||
- Build the solution before running tests to enable Visual Studio test discovery.
|
||||
@@ -1,6 +0,0 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "Wino-Mail"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
@@ -152,6 +152,7 @@ private string searchQuery = string.Empty;
|
||||
- ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`.
|
||||
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
|
||||
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
|
||||
- Never add XAML-backed UI controls to `.xaml.cs`. If a view has XAML, all control declarations, flyouts, templates, and visual composition belong in the `.xaml` file; keep `.xaml.cs` limited to event handling and view glue.
|
||||
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
|
||||
- If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`.
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using FluentAssertions;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Messaging.UI;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests.Services;
|
||||
|
||||
public sealed class MailRequestStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void MarkReadRequest_RevertUiChanges_RestoresOriginalReadState()
|
||||
{
|
||||
var mailCopy = CreateMailCopy(isRead: false, isFlagged: false);
|
||||
var request = new MarkReadRequest(mailCopy, IsRead: true);
|
||||
var recipient = new MailRequestRecipient();
|
||||
|
||||
WeakReferenceMessenger.Default.RegisterAll(recipient);
|
||||
|
||||
try
|
||||
{
|
||||
request.IsNoOp.Should().BeFalse();
|
||||
|
||||
request.ApplyUIChanges();
|
||||
request.RevertUIChanges();
|
||||
|
||||
mailCopy.IsRead.Should().BeFalse();
|
||||
recipient.Updated.Should().HaveCount(2);
|
||||
recipient.Updated[0].Source.Should().Be(MailUpdateSource.ClientUpdated);
|
||||
recipient.Updated[1].Source.Should().Be(MailUpdateSource.ClientReverted);
|
||||
recipient.Updated[1].UpdatedMail.IsRead.Should().BeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WeakReferenceMessenger.Default.UnregisterAll(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeFlagRequest_RevertUiChanges_RestoresOriginalFlagState()
|
||||
{
|
||||
var mailCopy = CreateMailCopy(isRead: true, isFlagged: false);
|
||||
var request = new ChangeFlagRequest(mailCopy, IsFlagged: true);
|
||||
var recipient = new MailRequestRecipient();
|
||||
|
||||
WeakReferenceMessenger.Default.RegisterAll(recipient);
|
||||
|
||||
try
|
||||
{
|
||||
request.IsNoOp.Should().BeFalse();
|
||||
|
||||
request.ApplyUIChanges();
|
||||
request.RevertUIChanges();
|
||||
|
||||
mailCopy.IsFlagged.Should().BeFalse();
|
||||
recipient.Updated.Should().HaveCount(2);
|
||||
recipient.Updated[0].Source.Should().Be(MailUpdateSource.ClientUpdated);
|
||||
recipient.Updated[1].Source.Should().Be(MailUpdateSource.ClientReverted);
|
||||
recipient.Updated[1].UpdatedMail.IsFlagged.Should().BeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WeakReferenceMessenger.Default.UnregisterAll(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private static MailCopy CreateMailCopy(bool isRead, bool isFlagged) =>
|
||||
new()
|
||||
{
|
||||
UniqueId = Guid.NewGuid(),
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
FolderId = Guid.NewGuid(),
|
||||
IsRead = isRead,
|
||||
IsFlagged = isFlagged
|
||||
};
|
||||
|
||||
internal sealed class MailRequestRecipient : IRecipient<MailUpdatedMessage>
|
||||
{
|
||||
public List<MailUpdatedMessage> Updated { get; } = [];
|
||||
|
||||
public void Receive(MailUpdatedMessage message) => Updated.Add(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Kiota.Abstractions;
|
||||
using Moq;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration.Processors;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Core.Synchronizers.Mail;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests.Synchronizers;
|
||||
|
||||
public sealed class OutlookSynchronizerRequestSuccessTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HandleSuccessfulResponseAsync_MarkReadRequest_PersistsLocalReadStateEvenWithoutResponseBody()
|
||||
{
|
||||
var changeProcessor = new Mock<IOutlookChangeProcessor>(MockBehavior.Strict);
|
||||
changeProcessor
|
||||
.Setup(x => x.ChangeMailReadStatusAsync("mail-id", true))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var synchronizer = CreateSynchronizer(changeProcessor.Object);
|
||||
var request = new MarkReadRequest(CreateMailCopy(), IsRead: true);
|
||||
var bundle = new HttpRequestBundle<RequestInformation>(new RequestInformation(), request, request);
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(string.Empty)
|
||||
};
|
||||
|
||||
await InvokeHandleSuccessfulResponseAsync(synchronizer, bundle, response);
|
||||
|
||||
changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-id", true), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleSuccessfulResponseAsync_ChangeFlagRequest_PersistsLocalFlagStateEvenWithoutResponseBody()
|
||||
{
|
||||
var changeProcessor = new Mock<IOutlookChangeProcessor>(MockBehavior.Strict);
|
||||
changeProcessor
|
||||
.Setup(x => x.ChangeFlagStatusAsync("mail-id", true))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var synchronizer = CreateSynchronizer(changeProcessor.Object);
|
||||
var request = new ChangeFlagRequest(CreateMailCopy(), IsFlagged: true);
|
||||
var bundle = new HttpRequestBundle<RequestInformation>(new RequestInformation(), request, request);
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(string.Empty)
|
||||
};
|
||||
|
||||
await InvokeHandleSuccessfulResponseAsync(synchronizer, bundle, response);
|
||||
|
||||
changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-id", true), Times.Once);
|
||||
}
|
||||
|
||||
private static OutlookSynchronizer CreateSynchronizer(IOutlookChangeProcessor changeProcessor)
|
||||
{
|
||||
var account = new MailAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Outlook",
|
||||
Address = "user@example.com"
|
||||
};
|
||||
|
||||
var authenticator = new Mock<IAuthenticator>(MockBehavior.Loose);
|
||||
var errorFactory = new Mock<IOutlookSynchronizerErrorHandlerFactory>(MockBehavior.Loose);
|
||||
|
||||
return new OutlookSynchronizer(account, authenticator.Object, changeProcessor, errorFactory.Object);
|
||||
}
|
||||
|
||||
private static MailCopy CreateMailCopy() =>
|
||||
new()
|
||||
{
|
||||
UniqueId = Guid.NewGuid(),
|
||||
Id = "mail-id",
|
||||
FolderId = Guid.NewGuid(),
|
||||
IsRead = false,
|
||||
IsFlagged = false
|
||||
};
|
||||
|
||||
private static async Task InvokeHandleSuccessfulResponseAsync(
|
||||
OutlookSynchronizer synchronizer,
|
||||
HttpRequestBundle<RequestInformation> bundle,
|
||||
HttpResponseMessage response)
|
||||
{
|
||||
var method = typeof(OutlookSynchronizer).GetMethod(
|
||||
"HandleSuccessfulResponseAsync",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var task = method!.Invoke(synchronizer, [bundle, response]) as Task;
|
||||
task.Should().NotBeNull();
|
||||
await task!;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ namespace Wino.Core.Requests.Mail;
|
||||
public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase(Item),
|
||||
ICustomFolderSynchronizationRequest
|
||||
{
|
||||
private readonly bool _originalIsFlagged = Item.IsFlagged;
|
||||
|
||||
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
||||
|
||||
public bool ExcludeMustHaveFolders => true;
|
||||
@@ -22,7 +24,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
|
||||
/// 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 bool IsNoOp { get; } = Item.IsFlagged == IsFlagged;
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
@@ -39,7 +41,7 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
|
||||
// Skip UI revert if this was a no-op request
|
||||
if (IsNoOp) return;
|
||||
|
||||
Item.IsFlagged = !IsFlagged;
|
||||
Item.IsFlagged = _originalIsFlagged;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace Wino.Core.Requests.Mail;
|
||||
|
||||
public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item), ICustomFolderSynchronizationRequest
|
||||
{
|
||||
private readonly bool _originalIsRead = Item.IsRead;
|
||||
|
||||
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
||||
|
||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.MarkRead;
|
||||
@@ -21,7 +23,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
|
||||
/// 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 bool IsNoOp { get; } = Item.IsRead == IsRead;
|
||||
|
||||
public override void ApplyUIChanges()
|
||||
{
|
||||
@@ -38,7 +40,7 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
|
||||
// Skip UI revert if this was a no-op request
|
||||
if (IsNoOp) return;
|
||||
|
||||
Item.IsRead = !IsRead;
|
||||
Item.IsRead = _originalIsRead;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, MailUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
|
||||
}
|
||||
|
||||
@@ -1884,6 +1884,18 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bundle?.UIChangeRequest is MarkReadRequest markReadRequest)
|
||||
{
|
||||
await _outlookChangeProcessor.ChangeMailReadStatusAsync(markReadRequest.Item.Id, markReadRequest.IsRead).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle?.UIChangeRequest is ChangeFlagRequest changeFlagRequest)
|
||||
{
|
||||
await _outlookChangeProcessor.ChangeFlagStatusAsync(changeFlagRequest.Item.Id, changeFlagRequest.IsFlagged).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Misc;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
@@ -263,7 +264,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption);
|
||||
}
|
||||
|
||||
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 1 : 0;
|
||||
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1;
|
||||
|
||||
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
|
||||
|
||||
@@ -300,7 +301,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
foreach (var calendar in calendars)
|
||||
{
|
||||
AccountCalendars.Add(calendar);
|
||||
AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions));
|
||||
AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions, AvailableColors));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -328,6 +329,15 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public async Task UpdateCalendarColorAsync(AccountCalendarSettingsItemViewModel calendarItem, AppColorViewModel color)
|
||||
{
|
||||
if (calendarItem?.Calendar == null || color == null || calendarItem.Calendar.BackgroundColorHex == color.Hex)
|
||||
return;
|
||||
|
||||
calendarItem.SetBackgroundColor(color);
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendarItem.Calendar);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetColor()
|
||||
=> SelectedColor = null;
|
||||
@@ -403,6 +413,7 @@ public partial class AccountCalendarSettingsItemViewModel : ObservableObject
|
||||
{
|
||||
public AccountCalendar Calendar { get; }
|
||||
public ObservableCollection<AccountCalendarShowAsOption> ShowAsOptions { get; }
|
||||
public List<AppColorViewModel> AvailableColors { get; }
|
||||
|
||||
public string Name => Calendar.Name;
|
||||
public string TimeZone => Calendar.TimeZone;
|
||||
@@ -414,11 +425,24 @@ public partial class AccountCalendarSettingsItemViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendarShowAsOption SelectedShowAsOption { get; set; }
|
||||
|
||||
public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection<AccountCalendarShowAsOption> showAsOptions)
|
||||
[ObservableProperty]
|
||||
public partial AppColorViewModel SelectedColor { get; set; }
|
||||
|
||||
public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection<AccountCalendarShowAsOption> showAsOptions, List<AppColorViewModel> availableColors)
|
||||
{
|
||||
Calendar = calendar;
|
||||
ShowAsOptions = showAsOptions;
|
||||
AvailableColors = availableColors;
|
||||
IsSynchronizationEnabled = calendar.IsSynchronizationEnabled;
|
||||
SelectedShowAsOption = showAsOptions.FirstOrDefault(option => option.ShowAs == calendar.DefaultShowAs) ?? showAsOptions.FirstOrDefault();
|
||||
SelectedColor = availableColors.FirstOrDefault(color => string.Equals(color.Hex, calendar.BackgroundColorHex, StringComparison.OrdinalIgnoreCase))
|
||||
?? new AppColorViewModel(calendar.BackgroundColorHex ?? ColorHelpers.GenerateFlatColorHex());
|
||||
}
|
||||
|
||||
public void SetBackgroundColor(AppColorViewModel color)
|
||||
{
|
||||
SelectedColor = color;
|
||||
Calendar.BackgroundColorHex = color.Hex;
|
||||
OnPropertyChanged(nameof(BackgroundColorHex));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Mail.ViewModels;
|
||||
using Wino.Views.Abstract;
|
||||
@@ -56,6 +57,14 @@ public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract
|
||||
}
|
||||
}
|
||||
|
||||
private async void CalendarColorItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (sender is GridView { Tag: AccountCalendarSettingsItemViewModel calendarItem } && e.ClickedItem is AppColorViewModel color)
|
||||
{
|
||||
await ViewModel.UpdateCalendarColorAsync(calendarItem, color);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
Reference in New Issue
Block a user