Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bba3523ec6 |
@@ -1,187 +0,0 @@
|
|||||||
name: Manual Beta Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_title:
|
|
||||||
description: Optional GitHub release title override
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-beta:
|
|
||||||
name: Build and publish beta release
|
|
||||||
runs-on: windows-latest
|
|
||||||
env:
|
|
||||||
PROJECT_PATH: Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
|
|
||||||
MANIFEST_PATH: Wino.Mail.WinUI/Package.appxmanifest
|
|
||||||
CHANGELOG_PATH: CHANGELOG.md
|
|
||||||
NUGET_CONFIG_PATH: ${{ github.workspace }}\nuget.config
|
|
||||||
PACKAGE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\package
|
|
||||||
RELEASE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\release
|
|
||||||
CERTIFICATE_PFX_PATH: ${{ github.workspace }}\artifacts\signing\beta-signing-cert.pfx
|
|
||||||
CERTIFICATE_CER_PATH: ${{ github.workspace }}\artifacts\release\Wino-Mail-Beta.cer
|
|
||||||
steps:
|
|
||||||
- name: Checkout selected branch
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Fetch tags from origin
|
|
||||||
shell: pwsh
|
|
||||||
run: git fetch origin --force --tags
|
|
||||||
|
|
||||||
- name: Validate release secrets
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
|
|
||||||
run: |
|
|
||||||
if ([string]::IsNullOrWhiteSpace($env:BETA_SIGNING_CERT_PFX_BASE64)) {
|
|
||||||
throw "Missing required secret: BETA_SIGNING_CERT_PFX_BASE64"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Setup .NET SDK
|
|
||||||
uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.0.x
|
|
||||||
env:
|
|
||||||
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Compute beta version and release metadata
|
|
||||||
id: metadata
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
RELEASE_TITLE_INPUT: ${{ github.event.inputs.release_title }}
|
|
||||||
run: |
|
|
||||||
$manifestPath = Join-Path $env:GITHUB_WORKSPACE $env:MANIFEST_PATH
|
|
||||||
if (-not (Test-Path $manifestPath)) {
|
|
||||||
throw "Package manifest not found: $manifestPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$changelogPath = Join-Path $env:GITHUB_WORKSPACE $env:CHANGELOG_PATH
|
|
||||||
if (-not (Test-Path $changelogPath)) {
|
|
||||||
throw "Release notes file not found: $changelogPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
[xml]$manifest = Get-Content -LiteralPath $manifestPath
|
|
||||||
$identityNode = $manifest.Package.Identity
|
|
||||||
if (-not $identityNode) {
|
|
||||||
throw "Could not locate the Package/Identity node in $manifestPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentVersionText = [string]$identityNode.Version
|
|
||||||
if ($currentVersionText -notmatch '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)\.(?<revision>\d+)$') {
|
|
||||||
throw "Manifest version '$currentVersionText' is not a four-part numeric version."
|
|
||||||
}
|
|
||||||
|
|
||||||
$packageVersion = $currentVersionText
|
|
||||||
$releaseTag = "v$packageVersion"
|
|
||||||
$releaseTitleInput = $env:RELEASE_TITLE_INPUT
|
|
||||||
$releaseTitle = if ([string]::IsNullOrWhiteSpace($releaseTitleInput)) { $releaseTag } else { $releaseTitleInput.Trim() }
|
|
||||||
|
|
||||||
$headSha = (git rev-parse HEAD).Trim()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($headSha)) {
|
|
||||||
throw "Failed to resolve the checked out commit SHA."
|
|
||||||
}
|
|
||||||
|
|
||||||
$notesInput = Get-Content -LiteralPath $changelogPath -Raw
|
|
||||||
if ([string]::IsNullOrWhiteSpace($notesInput)) {
|
|
||||||
throw "Release notes file is empty: $changelogPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$notesInput = $notesInput.Trim()
|
|
||||||
New-Item -ItemType Directory -Path $env:RELEASE_OUTPUT_DIR -Force | Out-Null
|
|
||||||
$releaseNotesPath = Join-Path $env:RELEASE_OUTPUT_DIR 'beta-release-notes.md'
|
|
||||||
$notesInput | Set-Content -LiteralPath $releaseNotesPath -Encoding utf8
|
|
||||||
|
|
||||||
"package_version=$packageVersion" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_tag=$releaseTag" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_title=$releaseTitle" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_notes_path=$releaseNotesPath" >> $env:GITHUB_OUTPUT
|
|
||||||
"head_sha=$headSha" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Materialize signing certificate
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
|
|
||||||
run: |
|
|
||||||
$signingDir = Split-Path -Parent $env:CERTIFICATE_PFX_PATH
|
|
||||||
New-Item -ItemType Directory -Path $signingDir -Force | Out-Null
|
|
||||||
[IO.File]::WriteAllBytes($env:CERTIFICATE_PFX_PATH, [Convert]::FromBase64String($env:BETA_SIGNING_CERT_PFX_BASE64))
|
|
||||||
|
|
||||||
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($env:CERTIFICATE_PFX_PATH, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path (Split-Path -Parent $env:CERTIFICATE_CER_PATH) -Force | Out-Null
|
|
||||||
[IO.File]::WriteAllBytes($env:CERTIFICATE_CER_PATH, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
|
|
||||||
|
|
||||||
- name: Restore WinUI project dependencies
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
if (-not (Test-Path $env:NUGET_CONFIG_PATH)) {
|
|
||||||
throw "NuGet config file not found: $env:NUGET_CONFIG_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
dotnet restore $env:PROJECT_PATH `
|
|
||||||
--configfile $env:NUGET_CONFIG_PATH `
|
|
||||||
-p:Platform=x64 `
|
|
||||||
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH"
|
|
||||||
|
|
||||||
- name: Build MSIX bundle
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
New-Item -ItemType Directory -Path $env:PACKAGE_OUTPUT_DIR -Force | Out-Null
|
|
||||||
|
|
||||||
dotnet build $env:PROJECT_PATH `
|
|
||||||
--configuration Release `
|
|
||||||
--no-restore `
|
|
||||||
--configfile $env:NUGET_CONFIG_PATH `
|
|
||||||
/p:Platform=x64 `
|
|
||||||
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH" `
|
|
||||||
/p:GenerateAppxPackageOnBuild=true `
|
|
||||||
/p:UapAppxPackageBuildMode=SideloadOnly `
|
|
||||||
/p:AppxBundle=Always `
|
|
||||||
/p:AppxBundlePlatforms="x86|x64|arm64" `
|
|
||||||
/p:AppxPackageDir="$env:PACKAGE_OUTPUT_DIR\\" `
|
|
||||||
/p:AppxPackageVersion=${{ steps.metadata.outputs.package_version }} `
|
|
||||||
/p:PackageCertificateKeyFile="$env:CERTIFICATE_PFX_PATH" `
|
|
||||||
/p:PackageCertificatePassword= `
|
|
||||||
/p:PackageCertificateThumbprint= `
|
|
||||||
/p:AppxPackageSigningEnabled=true
|
|
||||||
|
|
||||||
- name: Collect packaged artifacts
|
|
||||||
id: package
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$bundle = Get-ChildItem -Path $env:PACKAGE_OUTPUT_DIR -Recurse -Filter *.msixbundle | Select-Object -First 1
|
|
||||||
if (-not $bundle) {
|
|
||||||
throw "No .msixbundle file was generated under $env:PACKAGE_OUTPUT_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
$releaseAssetPath = Join-Path $env:RELEASE_OUTPUT_DIR "Wino_${{ steps.metadata.outputs.package_version }}.zip"
|
|
||||||
if (Test-Path $releaseAssetPath) {
|
|
||||||
Remove-Item -LiteralPath $releaseAssetPath -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
Compress-Archive -LiteralPath @($bundle.FullName, $env:CERTIFICATE_CER_PATH) -DestinationPath $releaseAssetPath -Force
|
|
||||||
|
|
||||||
"bundle_path=$($bundle.FullName)" >> $env:GITHUB_OUTPUT
|
|
||||||
"bundle_name=$($bundle.Name)" >> $env:GITHUB_OUTPUT
|
|
||||||
"release_asset_path=$releaseAssetPath" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create GitHub prerelease
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
gh release create "${{ steps.metadata.outputs.release_tag }}" `
|
|
||||||
"${{ steps.package.outputs.release_asset_path }}" `
|
|
||||||
--repo "${{ github.repository }}" `
|
|
||||||
--target "${{ steps.metadata.outputs.head_sha }}" `
|
|
||||||
--title "${{ steps.metadata.outputs.release_title }}" `
|
|
||||||
--notes-file "${{ steps.metadata.outputs.release_notes_path }}" `
|
|
||||||
--prerelease
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Released on April 15. Second bugfix/improvements update for v2 beta testing.
|
|
||||||
|
|
||||||
- [Ability to delete custom themes](https://github.com/bkaankose/Wino-Mail/issues/844)
|
|
||||||
- [[Proposal] Reply/Reply all sets focus to the "To" line versus Body](https://github.com/bkaankose/Wino-Mail/issues/844274)
|
|
||||||
- Email categories. Online sync for Outlook, offline use for IMAP/Gmail.
|
|
||||||
- Handling of read-only calendars.
|
|
||||||
- Implemented a new Github action workflow to trigger beta releases on demand.
|
|
||||||
@@ -178,6 +178,18 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
await InitializeAccountCalendarsAsync();
|
await InitializeAccountCalendarsAsync();
|
||||||
ValidateConfiguredNewEventCalendar();
|
ValidateConfiguredNewEventCalendar();
|
||||||
|
|
||||||
|
if (activationContext?.Parameter is CalendarItemTarget calendarItemTarget)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(WinoPage.EventDetailsPage, calendarItemTarget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activationContext?.Parameter is CalendarPageNavigationArgs calendarPageNavigationArgs)
|
||||||
|
{
|
||||||
|
NavigationService.Navigate(WinoPage.CalendarPage, calendarPageNavigationArgs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TodayClicked();
|
TodayClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (SelectedQuickEventAccountCalendar == null ||
|
if (SelectedQuickEventAccountCalendar == null ||
|
||||||
SelectedQuickEventAccountCalendar.IsReadOnly ||
|
|
||||||
SelectedQuickEventDate == null ||
|
SelectedQuickEventDate == null ||
|
||||||
string.IsNullOrWhiteSpace(EventName) ||
|
string.IsNullOrWhiteSpace(EventName) ||
|
||||||
string.IsNullOrWhiteSpace(SelectedStartTimeString) ||
|
string.IsNullOrWhiteSpace(SelectedStartTimeString) ||
|
||||||
@@ -205,12 +204,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
if (DisplayDetailsCalendarItemViewModel?.CalendarItem == null)
|
if (DisplayDetailsCalendarItemViewModel?.CalendarItem == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (DisplayDetailsCalendarItemViewModel.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DisplayDetailsCalendarItemViewModel.CalendarItem.IsRecurringParent)
|
if (DisplayDetailsCalendarItemViewModel.CalendarItem.IsRecurringParent)
|
||||||
{
|
{
|
||||||
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
||||||
@@ -467,12 +460,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
|
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
|
||||||
private async Task SaveQuickEventAsync()
|
private async Task SaveQuickEventAsync()
|
||||||
{
|
{
|
||||||
if (SelectedQuickEventAccountCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startDate = IsAllDay ? SelectedQuickEventDate.Value.Date : QuickEventStartTime;
|
var startDate = IsAllDay ? SelectedQuickEventDate.Value.Date : QuickEventStartTime;
|
||||||
var endDate = IsAllDay ? SelectedQuickEventDate.Value.Date.AddDays(1) : QuickEventEndTime;
|
var endDate = IsAllDay ? SelectedQuickEventDate.Value.Date.AddDays(1) : QuickEventEndTime;
|
||||||
var composeResult = new CalendarEventComposeResult
|
var composeResult = new CalendarEventComposeResult
|
||||||
@@ -566,12 +553,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calendarItem.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalizedTargetStart = calendarItem.IsAllDayEvent
|
var normalizedTargetStart = calendarItem.IsAllDayEvent
|
||||||
? targetStart.Date
|
? targetStart.Date
|
||||||
: targetStart;
|
: targetStart;
|
||||||
@@ -1214,12 +1195,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
if (targetItem == null)
|
if (targetItem == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (targetItem.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetItem.IsRecurringParent)
|
if (targetItem.IsRecurringParent)
|
||||||
{
|
{
|
||||||
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
||||||
@@ -1246,12 +1221,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
if (targetItem == null || targetItem.ShowAs == showAs)
|
if (targetItem == null || targetItem.ShowAs == showAs)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (targetItem.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var originalItem = await _calendarService.GetCalendarItemAsync(targetItem.Id).ConfigureAwait(false);
|
var originalItem = await _calendarService.GetCalendarItemAsync(targetItem.Id).ConfigureAwait(false);
|
||||||
var attendees = await _calendarService.GetAttendeesAsync(targetItem.Id).ConfigureAwait(false);
|
var attendees = await _calendarService.GetAttendeesAsync(targetItem.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1276,12 +1245,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
if (targetItem == null)
|
if (targetItem == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (targetItem.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var operation = responseStatus switch
|
var operation = responseStatus switch
|
||||||
{
|
{
|
||||||
CalendarItemStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
|
CalendarItemStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
|
||||||
|
|||||||
@@ -55,12 +55,6 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
|
|||||||
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReadOnly
|
|
||||||
{
|
|
||||||
get => AccountCalendar.IsReadOnly;
|
|
||||||
set => SetProperty(AccountCalendar.IsReadOnly, value, AccountCalendar, (u, i) => u.IsReadOnly = i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSynchronizationEnabled
|
public bool IsSynchronizationEnabled
|
||||||
{
|
{
|
||||||
get => AccountCalendar.IsSynchronizationEnabled;
|
get => AccountCalendar.IsSynchronizationEnabled;
|
||||||
|
|||||||
@@ -440,11 +440,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private async Task SaveAsync()
|
private async Task SaveAsync()
|
||||||
{
|
{
|
||||||
if (CurrentEvent == null) return;
|
if (CurrentEvent == null) return;
|
||||||
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -511,11 +506,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private async Task DeleteAsync()
|
private async Task DeleteAsync()
|
||||||
{
|
{
|
||||||
if (CurrentEvent == null) return;
|
if (CurrentEvent == null) return;
|
||||||
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the event is a master recurring event, ask for confirmation
|
// If the event is a master recurring event, ask for confirmation
|
||||||
if (CurrentEvent.IsRecurringParent)
|
if (CurrentEvent.IsRecurringParent)
|
||||||
@@ -620,11 +610,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
private async Task SendRsvpResponse(AttendeeStatus status)
|
private async Task SendRsvpResponse(AttendeeStatus status)
|
||||||
{
|
{
|
||||||
if (CurrentEvent == null) return;
|
if (CurrentEvent == null) return;
|
||||||
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public static class Constants
|
|||||||
public const string ToastCalendarJoinOnlineAction = nameof(ToastCalendarJoinOnlineAction);
|
public const string ToastCalendarJoinOnlineAction = nameof(ToastCalendarJoinOnlineAction);
|
||||||
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
|
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
|
||||||
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
|
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
|
||||||
|
public const string ToastCalendarSnoozeDurationMinutesKey = nameof(ToastCalendarSnoozeDurationMinutesKey);
|
||||||
public const string ToastModeKey = nameof(ToastModeKey);
|
public const string ToastModeKey = nameof(ToastModeKey);
|
||||||
public const string ToastModeMail = nameof(ToastModeMail);
|
public const string ToastModeMail = nameof(ToastModeMail);
|
||||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ public class AccountCalendar : IAccountCalendar
|
|||||||
public string SynchronizationDeltaToken { get; set; }
|
public string SynchronizationDeltaToken { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool IsPrimary { get; set; }
|
public bool IsPrimary { get; set; }
|
||||||
public bool IsReadOnly { get; set; }
|
|
||||||
public bool IsSynchronizationEnabled { get; set; } = true;
|
public bool IsSynchronizationEnabled { get; set; } = true;
|
||||||
public bool IsExtended { get; set; } = true;
|
public bool IsExtended { get; set; } = true;
|
||||||
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
|
||||||
|
|
||||||
public class MailCategory
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public Guid MailAccountId { get; set; }
|
|
||||||
|
|
||||||
public string RemoteId { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public bool IsFavorite { get; set; }
|
|
||||||
|
|
||||||
public string BackgroundColorHex { get; set; }
|
|
||||||
|
|
||||||
public string TextColorHex { get; set; }
|
|
||||||
|
|
||||||
public MailCategorySource Source { get; set; } = MailCategorySource.Local;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
|
||||||
|
|
||||||
public class MailCategoryAssignment
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public Guid MailCategoryId { get; set; }
|
|
||||||
|
|
||||||
public Guid MailCopyUniqueId { get; set; }
|
|
||||||
}
|
|
||||||
@@ -132,10 +132,5 @@ public class MailAccount
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail || ProviderType == MailProviderType.Outlook;
|
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail || ProviderType == MailProviderType.Outlook;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets whether the account can perform category definition sync type.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsCategorySyncSupported => ProviderType == MailProviderType.Outlook;
|
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
public enum MailCategorySource
|
|
||||||
{
|
|
||||||
Local,
|
|
||||||
Outlook
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ public enum MailSynchronizerOperation
|
|||||||
AlwaysMoveTo,
|
AlwaysMoveTo,
|
||||||
MoveToFocused,
|
MoveToFocused,
|
||||||
Archive,
|
Archive,
|
||||||
UpdateCategories,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FolderSynchronizerOperation
|
public enum FolderSynchronizerOperation
|
||||||
@@ -36,13 +35,6 @@ public enum CalendarSynchronizerOperation
|
|||||||
TentativeEvent,
|
TentativeEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CategorySynchronizerOperation
|
|
||||||
{
|
|
||||||
CreateCategory,
|
|
||||||
UpdateCategory,
|
|
||||||
DeleteCategory,
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI requests
|
// UI requests
|
||||||
public enum MailOperation
|
public enum MailOperation
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
public enum MailSynchronizationType
|
public enum MailSynchronizationType
|
||||||
{
|
{
|
||||||
UpdateProfile, // Only update profile information
|
UpdateProfile, // Only update profile information
|
||||||
Categories, // Only update mail categories
|
|
||||||
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
|
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
|
||||||
FoldersOnly, // Only synchronize folder metadata.
|
FoldersOnly, // Only synchronize folder metadata.
|
||||||
InboxOnly, // Only Inbox, Sent, Draft and Deleted folders.
|
InboxOnly, // Only Inbox, Sent, Draft and Deleted folders.
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public enum WinoPage
|
|||||||
AppPreferencesPage,
|
AppPreferencesPage,
|
||||||
SettingOptionsPage,
|
SettingOptionsPage,
|
||||||
AliasManagementPage,
|
AliasManagementPage,
|
||||||
MailCategoryManagementPage,
|
|
||||||
ImapCalDavSettingsPage,
|
ImapCalDavSettingsPage,
|
||||||
KeyboardShortcutsPage,
|
KeyboardShortcutsPage,
|
||||||
CalendarPage,
|
CalendarPage,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ public interface IAccountCalendar
|
|||||||
string TextColorHex { get; set; }
|
string TextColorHex { get; set; }
|
||||||
string BackgroundColorHex { get; set; }
|
string BackgroundColorHex { get; set; }
|
||||||
bool IsPrimary { get; set; }
|
bool IsPrimary { get; set; }
|
||||||
bool IsReadOnly { get; set; }
|
|
||||||
bool IsSynchronizationEnabled { get; set; }
|
bool IsSynchronizationEnabled { get; set; }
|
||||||
Guid AccountId { get; set; }
|
Guid AccountId { get; set; }
|
||||||
string RemoteCalendarId { get; set; }
|
string RemoteCalendarId { get; set; }
|
||||||
|
|||||||
@@ -14,22 +14,6 @@ public interface IFolderMenuItem : IBaseFolderMenuItem
|
|||||||
|
|
||||||
public interface IMergedAccountFolderMenuItem : IBaseFolderMenuItem { }
|
public interface IMergedAccountFolderMenuItem : IBaseFolderMenuItem { }
|
||||||
|
|
||||||
public interface IMailCategoryMenuItem : IBaseFolderMenuItem
|
|
||||||
{
|
|
||||||
Entities.Mail.MailCategory MailCategory { get; }
|
|
||||||
string TextColorHex { get; }
|
|
||||||
string BackgroundColorHex { get; }
|
|
||||||
bool HasTextColor { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IMergedMailCategoryMenuItem : IBaseFolderMenuItem
|
|
||||||
{
|
|
||||||
IReadOnlyList<Entities.Mail.MailCategory> Categories { get; }
|
|
||||||
string TextColorHex { get; }
|
|
||||||
string BackgroundColorHex { get; }
|
|
||||||
bool HasTextColor { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IBaseFolderMenuItem : IMenuItem
|
public interface IBaseFolderMenuItem : IMenuItem
|
||||||
{
|
{
|
||||||
string FolderName { get; }
|
string FolderName { get; }
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
public interface IMailCategoryService
|
|
||||||
{
|
|
||||||
Task<List<MailCategory>> GetCategoriesAsync(Guid accountId);
|
|
||||||
Task<List<MailCategory>> GetFavoriteCategoriesAsync(Guid accountId);
|
|
||||||
Task<MailCategory> GetCategoryAsync(Guid categoryId);
|
|
||||||
Task<bool> CategoryNameExistsAsync(Guid accountId, string name, Guid? excludedCategoryId = null);
|
|
||||||
Task<MailCategory> CreateCategoryAsync(MailCategory category);
|
|
||||||
Task UpdateCategoryAsync(MailCategory category);
|
|
||||||
Task DeleteCategoryAsync(Guid categoryId);
|
|
||||||
Task DeleteCategoriesAsync(Guid accountId);
|
|
||||||
Task ToggleFavoriteAsync(Guid categoryId, bool isFavorite);
|
|
||||||
Task UpdateRemoteIdAsync(Guid categoryId, string remoteId);
|
|
||||||
Task ReplaceCategoriesAsync(Guid accountId, IEnumerable<MailCategory> categories);
|
|
||||||
Task ReplaceMailAssignmentsAsync(Guid accountId, Guid mailCopyUniqueId, IEnumerable<string> categoryNames);
|
|
||||||
Task AssignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds);
|
|
||||||
Task UnassignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds);
|
|
||||||
Task<List<MailCategory>> GetCategoriesForMailAsync(Guid accountId, IEnumerable<Guid> mailCopyUniqueIds);
|
|
||||||
Task<List<Guid>> GetAssignedCategoryIdsForAllAsync(IEnumerable<Guid> mailCopyUniqueIds);
|
|
||||||
Task<List<string>> GetCategoryNamesForMailAsync(Guid mailCopyUniqueId);
|
|
||||||
Task<List<MailCopy>> GetMailCopiesForCategoryAsync(Guid categoryId);
|
|
||||||
Task<List<UnreadCategoryCountResult>> GetUnreadCategoryCountResultsAsync(IEnumerable<Guid> accountIds);
|
|
||||||
}
|
|
||||||
@@ -11,13 +11,11 @@ using Wino.Core.Domain.Models;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
public interface IMailDialogService : IDialogServiceBase
|
public interface IMailDialogService : IDialogServiceBase
|
||||||
{
|
{
|
||||||
void ShowReadOnlyCalendarMessage();
|
|
||||||
Task<bool> ShowHardDeleteConfirmationAsync();
|
Task<bool> ShowHardDeleteConfirmationAsync();
|
||||||
Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService);
|
Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService);
|
||||||
|
|
||||||
@@ -53,13 +51,6 @@ public interface IMailDialogService : IDialogServiceBase
|
|||||||
/// <returns>Created alias model if not canceled.</returns>
|
/// <returns>Created alias model if not canceled.</returns>
|
||||||
Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync();
|
Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Presents a dialog to the user for mail category creation/modification.
|
|
||||||
/// </summary>
|
|
||||||
#pragma warning disable CS8625
|
|
||||||
Task<MailCategoryDialogResult> ShowEditMailCategoryDialogAsync(MailCategory category = null);
|
|
||||||
#pragma warning restore CS8625
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Presents a dialog to the user to show email source.
|
/// Presents a dialog to the user to show email source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -72,9 +72,3 @@ public interface ICalendarActionRequest : IRequestBase
|
|||||||
Guid? LocalCalendarItemId { get; }
|
Guid? LocalCalendarItemId { get; }
|
||||||
CalendarSynchronizerOperation Operation { get; }
|
CalendarSynchronizerOperation Operation { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ICategoryActionRequest : IRequestBase
|
|
||||||
{
|
|
||||||
Guid AccountId { get; }
|
|
||||||
CategorySynchronizerOperation Operation { get; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,12 +63,6 @@ public interface ISynchronizationManager
|
|||||||
Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
|
Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles category synchronization for the given account.
|
|
||||||
/// </summary>
|
|
||||||
Task<MailSynchronizationResult> SynchronizeCategoriesAsync(Guid accountId,
|
|
||||||
CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles profile synchronization for the given account.
|
/// Handles profile synchronization for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
@@ -38,9 +36,4 @@ public interface IWinoRequestDelegator
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="calendarOperationPreparationRequest">Calendar preparation request.</param>
|
/// <param name="calendarOperationPreparationRequest">Calendar preparation request.</param>
|
||||||
Task ExecuteAsync(CalendarOperationPreparationRequest calendarOperationPreparationRequest);
|
Task ExecuteAsync(CalendarOperationPreparationRequest calendarOperationPreparationRequest);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Queues pre-built requests for a single account and triggers synchronization.
|
|
||||||
/// </summary>
|
|
||||||
Task ExecuteAsync(Guid accountId, IEnumerable<IRequestBase> requests);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Folders;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.MenuItems;
|
|
||||||
|
|
||||||
public partial class MailCategoryMenuItem : MenuItemBase<MailCategory, IMenuItem>, IFolderMenuItem, IMailCategoryMenuItem
|
|
||||||
{
|
|
||||||
private IReadOnlyList<IMailItemFolder> _handlingFolders;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private int unreadItemCount;
|
|
||||||
|
|
||||||
public MailCategoryMenuItem(MailCategory category, MailAccount parentAccount, IEnumerable<IMailItemFolder> handlingFolders, IMenuItem parentMenuItem)
|
|
||||||
: base(category, category.Id, parentMenuItem)
|
|
||||||
{
|
|
||||||
ParentAccount = parentAccount;
|
|
||||||
_handlingFolders = handlingFolders?.ToList() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FolderName => Parameter.Name;
|
|
||||||
public bool IsSynchronizationEnabled => false;
|
|
||||||
public SpecialFolderType SpecialFolderType => SpecialFolderType.Other;
|
|
||||||
public IEnumerable<IMailItemFolder> HandlingFolders => _handlingFolders;
|
|
||||||
public new ObservableCollection<IMenuItem> SubMenuItems { get; } = [];
|
|
||||||
public bool IsMoveTarget => true;
|
|
||||||
public bool IsSticky => false;
|
|
||||||
public bool IsSystemFolder => false;
|
|
||||||
public bool ShowUnreadCount => true;
|
|
||||||
public string AssignedAccountName => ParentAccount?.Name;
|
|
||||||
public MailAccount ParentAccount { get; private set; }
|
|
||||||
public string TextColorHex => Parameter.TextColorHex;
|
|
||||||
public string BackgroundColorHex => Parameter.BackgroundColorHex;
|
|
||||||
public bool HasTextColor => !string.IsNullOrWhiteSpace(Parameter.TextColorHex);
|
|
||||||
public MailCategory MailCategory => Parameter;
|
|
||||||
|
|
||||||
public void UpdateFolder(IMailItemFolder folder)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateParentAccounnt(MailAccount account) => ParentAccount = account;
|
|
||||||
}
|
|
||||||
@@ -22,13 +22,11 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
|
|
||||||
public IEnumerable<IAccountMenuItem> GetAllAccountMenuItems()
|
public IEnumerable<IAccountMenuItem> GetAllAccountMenuItems()
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
foreach (var item in this)
|
||||||
|
|
||||||
foreach (var item in rootItems)
|
|
||||||
{
|
{
|
||||||
if (item is MergedAccountMenuItem mergedAccountMenuItem)
|
if (item is MergedAccountMenuItem mergedAccountMenuItem)
|
||||||
{
|
{
|
||||||
foreach (var singleItem in mergedAccountMenuItem.SubMenuItems.OfType<IAccountMenuItem>().ToList())
|
foreach (var singleItem in mergedAccountMenuItem.SubMenuItems.OfType<IAccountMenuItem>())
|
||||||
{
|
{
|
||||||
yield return singleItem;
|
yield return singleItem;
|
||||||
}
|
}
|
||||||
@@ -42,11 +40,9 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
|
|
||||||
public IEnumerable<IBaseFolderMenuItem> GetAllFolderMenuItems(Guid folderId)
|
public IEnumerable<IBaseFolderMenuItem> GetAllFolderMenuItems(Guid folderId)
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
foreach (var item in this)
|
||||||
|
|
||||||
foreach (var item in rootItems)
|
|
||||||
{
|
{
|
||||||
if (item is IBaseFolderMenuItem folderMenuItem && item is not IMailCategoryMenuItem && item is not IMergedMailCategoryMenuItem)
|
if (item is IBaseFolderMenuItem folderMenuItem)
|
||||||
{
|
{
|
||||||
if (folderMenuItem.HandlingFolders.Any(a => a.Id == folderId))
|
if (folderMenuItem.HandlingFolders.Any(a => a.Id == folderId))
|
||||||
{
|
{
|
||||||
@@ -54,7 +50,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
}
|
}
|
||||||
else if (folderMenuItem.SubMenuItems.Any())
|
else if (folderMenuItem.SubMenuItems.Any())
|
||||||
{
|
{
|
||||||
foreach (var subItem in folderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>().ToList())
|
foreach (var subItem in folderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>())
|
||||||
{
|
{
|
||||||
if (subItem.HandlingFolders.Any(a => a.Id == folderId))
|
if (subItem.HandlingFolders.Any(a => a.Id == folderId))
|
||||||
{
|
{
|
||||||
@@ -69,10 +65,8 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
|
|
||||||
public bool TryGetAccountMenuItem(Guid accountId, out IAccountMenuItem value)
|
public bool TryGetAccountMenuItem(Guid accountId, out IAccountMenuItem value)
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
value = this.OfType<AccountMenuItem>().FirstOrDefault(a => a.AccountId == accountId);
|
||||||
|
value ??= this.OfType<MergedAccountMenuItem>().FirstOrDefault(a => a.SubMenuItems.OfType<AccountMenuItem>().Where(b => b.AccountId == accountId) != null);
|
||||||
value = rootItems.OfType<AccountMenuItem>().FirstOrDefault(a => a.AccountId == accountId);
|
|
||||||
value ??= rootItems.OfType<MergedAccountMenuItem>().FirstOrDefault(a => a.SubMenuItems.OfType<AccountMenuItem>().Any(b => b.AccountId == accountId));
|
|
||||||
|
|
||||||
return value != null;
|
return value != null;
|
||||||
}
|
}
|
||||||
@@ -80,9 +74,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
// Pattern: Look for special folder menu item inside the loaded folders for Windows Mail style menu items.
|
// Pattern: Look for special folder menu item inside the loaded folders for Windows Mail style menu items.
|
||||||
public bool TryGetWindowsStyleRootSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
public bool TryGetWindowsStyleRootSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
value = this.OfType<IBaseFolderMenuItem>()
|
||||||
|
|
||||||
value = rootItems.OfType<IBaseFolderMenuItem>()
|
|
||||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
|
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
|
||||||
|
|
||||||
return value != null;
|
return value != null;
|
||||||
@@ -92,9 +84,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
// This will not look for the folders inside individual account menu items inside merged account menu item.
|
// This will not look for the folders inside individual account menu items inside merged account menu item.
|
||||||
public bool TryGetMergedAccountSpecialFolderMenuItem(Guid mergedInboxId, SpecialFolderType specialFolderType, out IBaseFolderMenuItem value)
|
public bool TryGetMergedAccountSpecialFolderMenuItem(Guid mergedInboxId, SpecialFolderType specialFolderType, out IBaseFolderMenuItem value)
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
value = this.OfType<MergedAccountFolderMenuItem>()
|
||||||
|
|
||||||
value = rootItems.OfType<MergedAccountFolderMenuItem>()
|
|
||||||
.Where(a => a.MergedInbox.Id == mergedInboxId)
|
.Where(a => a.MergedInbox.Id == mergedInboxId)
|
||||||
.FirstOrDefault(a => a.SpecialFolderType == specialFolderType);
|
.FirstOrDefault(a => a.SpecialFolderType == specialFolderType);
|
||||||
|
|
||||||
@@ -103,14 +93,11 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
|
|
||||||
public bool TryGetFolderMenuItem(Guid folderId, out IBaseFolderMenuItem value)
|
public bool TryGetFolderMenuItem(Guid folderId, out IBaseFolderMenuItem value)
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
|
||||||
|
|
||||||
// Root folders
|
// Root folders
|
||||||
value = rootItems.OfType<IBaseFolderMenuItem>()
|
value = this.OfType<IBaseFolderMenuItem>()
|
||||||
.Where(a => a is not IMailCategoryMenuItem && a is not IMergedMailCategoryMenuItem)
|
|
||||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
||||||
|
|
||||||
value ??= rootItems.OfType<FolderMenuItem>()
|
value ??= this.OfType<FolderMenuItem>()
|
||||||
.SelectMany(a => a.SubMenuItems)
|
.SelectMany(a => a.SubMenuItems)
|
||||||
.OfType<IBaseFolderMenuItem>()
|
.OfType<IBaseFolderMenuItem>()
|
||||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
||||||
@@ -118,23 +105,10 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
return value != null;
|
return value != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetCategoryMenuItem(Guid categoryId, out IBaseFolderMenuItem value)
|
|
||||||
{
|
|
||||||
var rootItems = this.ToList();
|
|
||||||
|
|
||||||
value = rootItems.OfType<IMailCategoryMenuItem>()
|
|
||||||
.FirstOrDefault(a => a.MailCategory.Id == categoryId);
|
|
||||||
|
|
||||||
value ??= rootItems.OfType<IMergedMailCategoryMenuItem>()
|
|
||||||
.FirstOrDefault(a => a.Categories.Any(b => b.Id == categoryId)) as IBaseFolderMenuItem;
|
|
||||||
|
|
||||||
return value != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateUnreadItemCountsToZero()
|
public void UpdateUnreadItemCountsToZero()
|
||||||
{
|
{
|
||||||
// Handle the root folders.
|
// Handle the root folders.
|
||||||
foreach (var item in this.OfType<IBaseFolderMenuItem>().ToList())
|
foreach (var item in this.OfType<IBaseFolderMenuItem>())
|
||||||
{
|
{
|
||||||
RecursivelyResetUnreadItemCount(item);
|
RecursivelyResetUnreadItemCount(item);
|
||||||
}
|
}
|
||||||
@@ -146,7 +120,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
|
|
||||||
if (baseFolderMenuItem.SubMenuItems == null) return;
|
if (baseFolderMenuItem.SubMenuItems == null) return;
|
||||||
|
|
||||||
foreach (var subMenuItem in baseFolderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>().ToList())
|
foreach (var subMenuItem in baseFolderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>())
|
||||||
{
|
{
|
||||||
RecursivelyResetUnreadItemCount(subMenuItem);
|
RecursivelyResetUnreadItemCount(subMenuItem);
|
||||||
}
|
}
|
||||||
@@ -154,9 +128,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
|
|
||||||
public bool TryGetSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
public bool TryGetSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
|
||||||
{
|
{
|
||||||
var rootItems = this.ToList();
|
value = this.OfType<IBaseFolderMenuItem>()
|
||||||
|
|
||||||
value = rootItems.OfType<IBaseFolderMenuItem>()
|
|
||||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
|
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
|
||||||
|
|
||||||
return value != null;
|
return value != null;
|
||||||
@@ -170,12 +142,11 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
public AccountMenuItem GetSpecificAccountMenuItem(Guid accountId)
|
public AccountMenuItem GetSpecificAccountMenuItem(Guid accountId)
|
||||||
{
|
{
|
||||||
AccountMenuItem accountMenuItem = null;
|
AccountMenuItem accountMenuItem = null;
|
||||||
var rootItems = this.ToList();
|
|
||||||
|
|
||||||
accountMenuItem = rootItems.OfType<AccountMenuItem>().FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId));
|
accountMenuItem = this.OfType<AccountMenuItem>().FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId));
|
||||||
|
|
||||||
// Look for the items inside the merged accounts if regular menu item is not found.
|
// Look for the items inside the merged accounts if regular menu item is not found.
|
||||||
accountMenuItem ??= rootItems.OfType<MergedAccountMenuItem>()
|
accountMenuItem ??= this.OfType<MergedAccountMenuItem>()
|
||||||
.FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId))?.SubMenuItems
|
.FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId))?.SubMenuItems
|
||||||
.OfType<AccountMenuItem>()
|
.OfType<AccountMenuItem>()
|
||||||
.FirstOrDefault(a => a.AccountId == accountId);
|
.FirstOrDefault(a => a.AccountId == accountId);
|
||||||
@@ -196,7 +167,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
/// <param name="isEnabled">Whether menu items should be enabled or disabled.</param>
|
/// <param name="isEnabled">Whether menu items should be enabled or disabled.</param>
|
||||||
public async Task SetAccountMenuItemEnabledStatusAsync(bool isEnabled)
|
public async Task SetAccountMenuItemEnabledStatusAsync(bool isEnabled)
|
||||||
{
|
{
|
||||||
var accountItems = this.Where(a => a is IAccountMenuItem).Cast<IAccountMenuItem>().ToList();
|
var accountItems = this.Where(a => a is IAccountMenuItem).Cast<IAccountMenuItem>();
|
||||||
|
|
||||||
await _dispatcher.ExecuteOnUIThread(() =>
|
await _dispatcher.ExecuteOnUIThread(() =>
|
||||||
{
|
{
|
||||||
@@ -221,7 +192,6 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
{
|
{
|
||||||
// Check root-level items.
|
// Check root-level items.
|
||||||
var rootItem = this.OfType<IBaseFolderMenuItem>()
|
var rootItem = this.OfType<IBaseFolderMenuItem>()
|
||||||
.Where(a => a is not IMailCategoryMenuItem && a is not IMergedMailCategoryMenuItem)
|
|
||||||
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
|
||||||
|
|
||||||
if (rootItem != null)
|
if (rootItem != null)
|
||||||
@@ -231,7 +201,7 @@ public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check sub-items of root folders.
|
// Check sub-items of root folders.
|
||||||
foreach (var rootFolder in this.OfType<IBaseFolderMenuItem>().ToList())
|
foreach (var rootFolder in this.OfType<IBaseFolderMenuItem>())
|
||||||
{
|
{
|
||||||
var subItem = rootFolder.SubMenuItems
|
var subItem = rootFolder.SubMenuItems
|
||||||
.OfType<IBaseFolderMenuItem>()
|
.OfType<IBaseFolderMenuItem>()
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Folders;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.MenuItems;
|
|
||||||
|
|
||||||
public partial class MergedMailCategoryMenuItem : MenuItemBase<List<MailCategory>, IMenuItem>, IMergedAccountFolderMenuItem, IMergedMailCategoryMenuItem
|
|
||||||
{
|
|
||||||
private readonly IReadOnlyList<IMailItemFolder> _handlingFolders;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private int unreadItemCount;
|
|
||||||
|
|
||||||
public MergedMailCategoryMenuItem(List<MailCategory> categories, IEnumerable<IMailItemFolder> handlingFolders, MergedInbox mergedInbox)
|
|
||||||
: base(categories, null, null)
|
|
||||||
{
|
|
||||||
_handlingFolders = handlingFolders?.ToList() ?? [];
|
|
||||||
MergedInbox = mergedInbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FolderName => Parameter.FirstOrDefault()?.Name ?? string.Empty;
|
|
||||||
public bool IsSynchronizationEnabled => false;
|
|
||||||
public SpecialFolderType SpecialFolderType => SpecialFolderType.Other;
|
|
||||||
public IEnumerable<IMailItemFolder> HandlingFolders => _handlingFolders;
|
|
||||||
public bool IsMoveTarget => true;
|
|
||||||
public bool IsSticky => false;
|
|
||||||
public bool IsSystemFolder => false;
|
|
||||||
public bool ShowUnreadCount => true;
|
|
||||||
public string AssignedAccountName => MergedInbox?.Name;
|
|
||||||
public MergedInbox MergedInbox { get; }
|
|
||||||
public string TextColorHex => Parameter.FirstOrDefault()?.TextColorHex;
|
|
||||||
public string BackgroundColorHex => Parameter.FirstOrDefault()?.BackgroundColorHex;
|
|
||||||
public bool HasTextColor => !string.IsNullOrWhiteSpace(TextColorHex);
|
|
||||||
public IReadOnlyList<MailCategory> Categories => Parameter;
|
|
||||||
|
|
||||||
public void UpdateFolder(IMailItemFolder folder)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts;
|
|
||||||
|
|
||||||
public class UnreadCategoryCountResult
|
|
||||||
{
|
|
||||||
public Guid CategoryId { get; set; }
|
|
||||||
public Guid AccountId { get; set; }
|
|
||||||
public int UnreadItemCount { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Models.MailItem;
|
|
||||||
|
|
||||||
public sealed record MailCategoryColorOption(string BackgroundColorHex, string TextColorHex);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Models.MailItem;
|
|
||||||
|
|
||||||
public sealed record MailCategoryDialogResult(string Name, string BackgroundColorHex, string TextColorHex);
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.MailItem;
|
|
||||||
|
|
||||||
public static class MailCategoryPalette
|
|
||||||
{
|
|
||||||
public static IReadOnlyList<MailCategoryColorOption> DefaultOptions { get; } =
|
|
||||||
[
|
|
||||||
new("#FEE2E2", "#991B1B"),
|
|
||||||
new("#FECACA", "#7F1D1D"),
|
|
||||||
new("#FFEDD5", "#9A3412"),
|
|
||||||
new("#FED7AA", "#7C2D12"),
|
|
||||||
new("#FEF3C7", "#92400E"),
|
|
||||||
new("#FDE68A", "#78350F"),
|
|
||||||
new("#ECFCCB", "#3F6212"),
|
|
||||||
new("#D9F99D", "#365314"),
|
|
||||||
new("#DCFCE7", "#166534"),
|
|
||||||
new("#BBF7D0", "#14532D"),
|
|
||||||
new("#CCFBF1", "#115E59"),
|
|
||||||
new("#99F6E4", "#134E4A"),
|
|
||||||
new("#CFFAFE", "#155E75"),
|
|
||||||
new("#A5F3FC", "#164E63"),
|
|
||||||
new("#DBEAFE", "#1D4ED8"),
|
|
||||||
new("#BFDBFE", "#1E3A8A"),
|
|
||||||
new("#E0E7FF", "#4338CA"),
|
|
||||||
new("#DDD6FE", "#5B21B6"),
|
|
||||||
new("#F3E8FF", "#7E22CE"),
|
|
||||||
new("#FCE7F3", "#9D174D")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -9,5 +9,4 @@ public record NewMailItemPackage(
|
|||||||
MailCopy Copy,
|
MailCopy Copy,
|
||||||
MimeMessage Mime,
|
MimeMessage Mime,
|
||||||
string AssignedRemoteFolderId,
|
string AssignedRemoteFolderId,
|
||||||
IReadOnlyList<AccountContact> ExtractedContacts = null,
|
IReadOnlyList<AccountContact> ExtractedContacts = null);
|
||||||
IReadOnlyList<string> CategoryNames = null);
|
|
||||||
|
|||||||
@@ -17,8 +17,4 @@ public record MailListInitializationOptions(IEnumerable<IMailItemFolder> Folders
|
|||||||
List<MailCopy> PreFetchMailCopies = null,
|
List<MailCopy> PreFetchMailCopies = null,
|
||||||
bool DeduplicateByServerId = false,
|
bool DeduplicateByServerId = false,
|
||||||
int Skip = 0,
|
int Skip = 0,
|
||||||
int Take = 0)
|
int Take = 0);
|
||||||
{
|
|
||||||
public IReadOnlyList<Guid> CategoryIds { get; init; }
|
|
||||||
public bool IsCategoryView => CategoryIds?.Count > 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ public abstract record CalendarRequestBase(CalendarItem Item) : RequestBase<Cale
|
|||||||
public virtual Guid? LocalCalendarItemId => Item?.Id;
|
public virtual Guid? LocalCalendarItemId => Item?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract record CategoryRequestBase(Guid AccountId) : RequestBase<CategorySynchronizerOperation>, ICategoryActionRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BatchCollection<TRequestType> : List<TRequestType>, IUIChangeRequest where TRequestType : IUIChangeRequest
|
public class BatchCollection<TRequestType> : List<TRequestType>, IUIChangeRequest where TRequestType : IUIChangeRequest
|
||||||
{
|
{
|
||||||
public BatchCollection(IEnumerable<TRequestType> collection) : base(collection)
|
public BatchCollection(IEnumerable<TRequestType> collection) : base(collection)
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ public static class SettingsNavigationInfoProvider
|
|||||||
WinoPage.AccountDetailsPage => WinoPage.ManageAccountsPage,
|
WinoPage.AccountDetailsPage => WinoPage.ManageAccountsPage,
|
||||||
WinoPage.MergedAccountDetailsPage => WinoPage.ManageAccountsPage,
|
WinoPage.MergedAccountDetailsPage => WinoPage.ManageAccountsPage,
|
||||||
WinoPage.AliasManagementPage => WinoPage.ManageAccountsPage,
|
WinoPage.AliasManagementPage => WinoPage.ManageAccountsPage,
|
||||||
WinoPage.MailCategoryManagementPage => WinoPage.ManageAccountsPage,
|
|
||||||
WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage,
|
WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage,
|
||||||
WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage,
|
WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage,
|
||||||
WinoPage.CreateEmailTemplatePage => WinoPage.EmailTemplatesPage,
|
WinoPage.CreateEmailTemplatePage => WinoPage.EmailTemplatesPage,
|
||||||
|
|||||||
@@ -67,7 +67,6 @@
|
|||||||
"BasicIMAPSetupDialog_Password": "Password",
|
"BasicIMAPSetupDialog_Password": "Password",
|
||||||
"BasicIMAPSetupDialog_Title": "IMAP Account",
|
"BasicIMAPSetupDialog_Title": "IMAP Account",
|
||||||
"Busy": "Busy",
|
"Busy": "Busy",
|
||||||
"Buttons_Add": "Add",
|
|
||||||
"Buttons_AddAccount": "Add Account",
|
"Buttons_AddAccount": "Add Account",
|
||||||
"Buttons_FixAccount": "Fix Account",
|
"Buttons_FixAccount": "Fix Account",
|
||||||
"Buttons_AddNewAlias": "Add New Alias",
|
"Buttons_AddNewAlias": "Add New Alias",
|
||||||
@@ -214,8 +213,6 @@
|
|||||||
"CalendarEventDetails_Organizer": "Organizer",
|
"CalendarEventDetails_Organizer": "Organizer",
|
||||||
"CalendarEventDetails_People": "People",
|
"CalendarEventDetails_People": "People",
|
||||||
"CalendarEventDetails_ReadOnlyEvent": "Read-only event",
|
"CalendarEventDetails_ReadOnlyEvent": "Read-only event",
|
||||||
"CalendarReadOnly_Title": "Read-only calendar",
|
|
||||||
"CalendarReadOnly_Message": "You can't update this calendar or its events. This calendar is read-only.",
|
|
||||||
"CalendarContextMenu_Respond": "Respond",
|
"CalendarContextMenu_Respond": "Respond",
|
||||||
"CalendarEventDetails_Reminder": "Reminder",
|
"CalendarEventDetails_Reminder": "Reminder",
|
||||||
"CalendarReminder_StartedHoursAgo": "Started {0} hours ago",
|
"CalendarReminder_StartedHoursAgo": "Started {0} hours ago",
|
||||||
@@ -878,28 +875,10 @@
|
|||||||
"SettingsManageAccountSettings_Title": "Manage Accounts",
|
"SettingsManageAccountSettings_Title": "Manage Accounts",
|
||||||
"SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.",
|
"SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.",
|
||||||
"SettingsManageAliases_Title": "Aliases",
|
"SettingsManageAliases_Title": "Aliases",
|
||||||
"SettingsMailCategories_Description": "Manage synchronized and local categories for this account.",
|
|
||||||
"SettingsMailCategories_Title": "Categories",
|
|
||||||
"SettingsEditAccountDetails_Title": "Edit Account Details",
|
"SettingsEditAccountDetails_Title": "Edit Account Details",
|
||||||
"SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.",
|
"SettingsEditAccountDetails_Description": "Change account name, sender name and assign a new color if you like.",
|
||||||
"EditAccountDetailsPage_SaveSuccess_Title": "Changes Saved",
|
"EditAccountDetailsPage_SaveSuccess_Title": "Changes Saved",
|
||||||
"EditAccountDetailsPage_SaveSuccess_Message": "Your account details have been updated successfully.",
|
"EditAccountDetailsPage_SaveSuccess_Message": "Your account details have been updated successfully.",
|
||||||
"MailCategoryManagementPage_Title": "Categories",
|
|
||||||
"MailCategoryManagementPage_Description": "Create, edit, delete, and favorite categories for this account.",
|
|
||||||
"MailCategoryManagementPage_Empty": "No categories yet.",
|
|
||||||
"MailCategoryManagementPage_DeleteConfirmationTitle": "Delete Category",
|
|
||||||
"MailCategoryManagementPage_DeleteConfirmationMessage": "Delete category \"{0}\"?",
|
|
||||||
"MailCategoryManagementPage_RefreshConfirmationMessage": "This will delete all your local categories, and re-synchronize everything from the server. Do you want to continue?",
|
|
||||||
"MailCategoryMenuItem": "Category",
|
|
||||||
"MailCategoryDialog_CreateTitle": "Create category",
|
|
||||||
"MailCategoryDialog_EditTitle": "Edit category",
|
|
||||||
"MailCategoryDialog_Name": "Name",
|
|
||||||
"MailCategoryDialog_NamePlaceholder": "Category name",
|
|
||||||
"MailCategoryDialog_Color": "Color",
|
|
||||||
"MailCategoryDialog_InvalidNameTitle": "Category name required",
|
|
||||||
"MailCategoryDialog_InvalidNameMessage": "Enter a category name to continue.",
|
|
||||||
"MailCategoryDialog_DuplicateTitle": "Category already exists",
|
|
||||||
"MailCategoryDialog_DuplicateMessage": "A category with the same name already exists for this account.",
|
|
||||||
"SettingsManageLink_Description": "Move items to add new link or remove existing link.",
|
"SettingsManageLink_Description": "Move items to add new link or remove existing link.",
|
||||||
"SettingsManageLink_Title": "Manage Link",
|
"SettingsManageLink_Title": "Manage Link",
|
||||||
"SettingsMarkAsRead_Description": "Change what should happen to the selected item.",
|
"SettingsMarkAsRead_Description": "Change what should happen to the selected item.",
|
||||||
@@ -1512,13 +1491,11 @@
|
|||||||
"AccountSetup_Step_TestingCalendarAuth": "Testing calendar authentication",
|
"AccountSetup_Step_TestingCalendarAuth": "Testing calendar authentication",
|
||||||
"AccountSetup_Step_SavingAccount": "Saving account information",
|
"AccountSetup_Step_SavingAccount": "Saving account information",
|
||||||
"AccountSetup_Step_FetchingCalendarMetadata": "Fetching calendar metadata",
|
"AccountSetup_Step_FetchingCalendarMetadata": "Fetching calendar metadata",
|
||||||
"AccountSetup_Step_SyncingCategories": "Synchronizing categories",
|
|
||||||
"AccountSetup_Step_SyncingAliases": "Synchronizing aliases",
|
"AccountSetup_Step_SyncingAliases": "Synchronizing aliases",
|
||||||
"AccountSetup_Step_Finalizing": "Finalizing setup",
|
"AccountSetup_Step_Finalizing": "Finalizing setup",
|
||||||
"AccountSetup_FailureMessage": "Setup failed. Go back to fix your settings, or try again later.",
|
"AccountSetup_FailureMessage": "Setup failed. Go back to fix your settings, or try again later.",
|
||||||
"AccountSetup_SuccessMessage": "Your account has been set up successfully!",
|
"AccountSetup_SuccessMessage": "Your account has been set up successfully!",
|
||||||
"AccountSetup_GoBackButton": "Go Back",
|
"AccountSetup_GoBackButton": "Go Back",
|
||||||
"AccountSetup_TryAgainButton": "Try Again",
|
"AccountSetup_TryAgainButton": "Try Again",
|
||||||
"Exception_FailedToSynchronizeCategories": "Failed to synchronize categories",
|
|
||||||
"ImapCalDavSettings_AutoDiscoveryFailed": "Auto-discovery failed. Please enter settings manually in the Advanced tab."
|
"ImapCalDavSettings_AutoDiscoveryFailed": "Auto-discovery failed. Please enter settings manually in the Advanced tab."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -510,8 +510,7 @@ public class MailFetchingTests : IAsyncLifetime
|
|||||||
preferencesService.Object,
|
preferencesService.Object,
|
||||||
contactPictureFileService.Object);
|
contactPictureFileService.Object);
|
||||||
|
|
||||||
var mailCategoryService = new MailCategoryService(db);
|
var folderService = new FolderService(db, accountService);
|
||||||
var folderService = new FolderService(db, accountService, mailCategoryService);
|
|
||||||
var contactService = new ContactService(db);
|
var contactService = new ContactService(db);
|
||||||
var sentMailReceiptService = new SentMailReceiptService(db, folderService, accountService);
|
var sentMailReceiptService = new SentMailReceiptService(db, folderService, accountService);
|
||||||
|
|
||||||
@@ -523,7 +522,6 @@ public class MailFetchingTests : IAsyncLifetime
|
|||||||
signatureService.Object,
|
signatureService.Object,
|
||||||
mimeFileService.Object,
|
mimeFileService.Object,
|
||||||
preferencesService.Object,
|
preferencesService.Object,
|
||||||
sentMailReceiptService,
|
sentMailReceiptService);
|
||||||
mailCategoryService);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,8 +269,7 @@ public class MailThreadingTests : IAsyncLifetime
|
|||||||
preferencesService.Object,
|
preferencesService.Object,
|
||||||
contactPictureFileService.Object);
|
contactPictureFileService.Object);
|
||||||
|
|
||||||
var mailCategoryService = new MailCategoryService(db);
|
var folderService = new FolderService(db, accountService);
|
||||||
var folderService = new FolderService(db, accountService, mailCategoryService);
|
|
||||||
var contactService = new ContactService(db);
|
var contactService = new ContactService(db);
|
||||||
var sentMailReceiptService = new SentMailReceiptService(db, folderService, accountService);
|
var sentMailReceiptService = new SentMailReceiptService(db, folderService, accountService);
|
||||||
|
|
||||||
@@ -282,7 +281,6 @@ public class MailThreadingTests : IAsyncLifetime
|
|||||||
signatureService.Object,
|
signatureService.Object,
|
||||||
mimeFileService.Object,
|
mimeFileService.Object,
|
||||||
preferencesService.Object,
|
preferencesService.Object,
|
||||||
sentMailReceiptService,
|
sentMailReceiptService);
|
||||||
mailCategoryService);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,8 @@ public sealed class OutlookSynchronizerRequestSuccessTests
|
|||||||
|
|
||||||
var authenticator = new Mock<IAuthenticator>(MockBehavior.Loose);
|
var authenticator = new Mock<IAuthenticator>(MockBehavior.Loose);
|
||||||
var errorFactory = new Mock<IOutlookSynchronizerErrorHandlerFactory>(MockBehavior.Loose);
|
var errorFactory = new Mock<IOutlookSynchronizerErrorHandlerFactory>(MockBehavior.Loose);
|
||||||
var mailCategoryService = new Mock<IMailCategoryService>(MockBehavior.Loose);
|
|
||||||
|
|
||||||
return new OutlookSynchronizer(account, authenticator.Object, changeProcessor, errorFactory.Object, mailCategoryService.Object);
|
return new OutlookSynchronizer(account, authenticator.Object, changeProcessor, errorFactory.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MailCopy CreateMailCopy() =>
|
private static MailCopy CreateMailCopy() =>
|
||||||
|
|||||||
@@ -145,8 +145,6 @@ public static class GoogleIntegratorExtensions
|
|||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
TimeZone = calendarListEntry.TimeZone,
|
TimeZone = calendarListEntry.TimeZone,
|
||||||
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
||||||
IsReadOnly = !string.Equals(calendarListEntry.AccessRole, "owner", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.Equals(calendarListEntry.AccessRole, "writer", StringComparison.OrdinalIgnoreCase),
|
|
||||||
IsSynchronizationEnabled = true,
|
IsSynchronizationEnabled = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,6 @@ public static class OutlookIntegratorExtensions
|
|||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
RemoteCalendarId = outlookCalendar.Id,
|
RemoteCalendarId = outlookCalendar.Id,
|
||||||
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
|
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
|
||||||
IsReadOnly = !outlookCalendar.CanEdit.GetValueOrDefault(true),
|
|
||||||
Name = outlookCalendar.Name,
|
Name = outlookCalendar.Name,
|
||||||
IsSynchronizationEnabled = true,
|
IsSynchronizationEnabled = true,
|
||||||
IsExtended = true,
|
IsExtended = true,
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Models.Requests;
|
|
||||||
|
|
||||||
namespace Wino.Core.Requests.Category;
|
|
||||||
|
|
||||||
public record MailCategoryCreateRequest(MailCategory Category) : CategoryRequestBase(Category.MailAccountId)
|
|
||||||
{
|
|
||||||
public override CategorySynchronizerOperation Operation => CategorySynchronizerOperation.CreateCategory;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Models.Requests;
|
|
||||||
|
|
||||||
namespace Wino.Core.Requests.Category;
|
|
||||||
|
|
||||||
public record MailCategoryDeleteRequest(
|
|
||||||
MailCategory Category,
|
|
||||||
string PreviousRemoteId,
|
|
||||||
IReadOnlyList<MailCategoryMessageUpdateTarget> AffectedMessages = null) : CategoryRequestBase(Category.MailAccountId)
|
|
||||||
{
|
|
||||||
public override CategorySynchronizerOperation Operation => CategorySynchronizerOperation.DeleteCategory;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Wino.Core.Requests.Category;
|
|
||||||
|
|
||||||
public sealed record MailCategoryMessageUpdateTarget(string MessageId, IReadOnlyList<string> CategoryNames);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Models.Requests;
|
|
||||||
|
|
||||||
namespace Wino.Core.Requests.Category;
|
|
||||||
|
|
||||||
public record MailCategoryUpdateRequest(
|
|
||||||
MailCategory Category,
|
|
||||||
string PreviousName,
|
|
||||||
string PreviousRemoteId,
|
|
||||||
IReadOnlyList<MailCategoryMessageUpdateTarget> AffectedMessages = null) : CategoryRequestBase(Category.MailAccountId)
|
|
||||||
{
|
|
||||||
public override CategorySynchronizerOperation Operation => CategorySynchronizerOperation.UpdateCategory;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Requests;
|
|
||||||
|
|
||||||
namespace Wino.Core.Requests.Mail;
|
|
||||||
|
|
||||||
public record MailCategoryAssignmentRequest(
|
|
||||||
MailCopy Item,
|
|
||||||
Guid MailCategoryId,
|
|
||||||
string CategoryName,
|
|
||||||
IReadOnlyList<string> CategoryNames,
|
|
||||||
bool IsAssigned) : MailRequestBase(Item), ICustomFolderSynchronizationRequest
|
|
||||||
{
|
|
||||||
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.UpdateCategories;
|
|
||||||
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
|
|
||||||
public bool ExcludeMustHaveFolders => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BatchMailCategoryAssignmentRequest : BatchCollection<MailCategoryAssignmentRequest>
|
|
||||||
{
|
|
||||||
public BatchMailCategoryAssignmentRequest(IEnumerable<MailCategoryAssignmentRequest> collection) : base(collection)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -370,26 +370,6 @@ public class SynchronizationManager : ISynchronizationManager, IRecipient<Accoun
|
|||||||
return await SynchronizeMailAsync(options, cancellationToken);
|
return await SynchronizeMailAsync(options, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles category synchronization for the given account.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accountId">Account ID to synchronize categories for</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <returns>Synchronization result</returns>
|
|
||||||
public async Task<MailSynchronizationResult> SynchronizeCategoriesAsync(Guid accountId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
EnsureInitialized();
|
|
||||||
|
|
||||||
var options = new MailSynchronizationOptions
|
|
||||||
{
|
|
||||||
AccountId = accountId,
|
|
||||||
Type = MailSynchronizationType.Categories
|
|
||||||
};
|
|
||||||
|
|
||||||
return await SynchronizeMailAsync(options, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles profile synchronization for the given account.
|
/// Handles profile synchronization for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public class SynchronizerFactory : ISynchronizerFactory
|
|||||||
private readonly ICalDavClient _calDavClient;
|
private readonly ICalDavClient _calDavClient;
|
||||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||||
private readonly ICalendarService _calendarService;
|
private readonly ICalendarService _calendarService;
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
|
|
||||||
private readonly List<IWinoSynchronizerBase> synchronizerCache = new();
|
private readonly List<IWinoSynchronizerBase> synchronizerCache = new();
|
||||||
|
|
||||||
@@ -42,8 +41,7 @@ public class SynchronizerFactory : ISynchronizerFactory
|
|||||||
UnifiedImapSynchronizer unifiedImapSynchronizer,
|
UnifiedImapSynchronizer unifiedImapSynchronizer,
|
||||||
ICalDavClient calDavClient,
|
ICalDavClient calDavClient,
|
||||||
IAutoDiscoveryService autoDiscoveryService,
|
IAutoDiscoveryService autoDiscoveryService,
|
||||||
ICalendarService calendarService,
|
ICalendarService calendarService)
|
||||||
IMailCategoryService mailCategoryService)
|
|
||||||
{
|
{
|
||||||
_outlookChangeProcessor = outlookChangeProcessor;
|
_outlookChangeProcessor = outlookChangeProcessor;
|
||||||
_gmailChangeProcessor = gmailChangeProcessor;
|
_gmailChangeProcessor = gmailChangeProcessor;
|
||||||
@@ -58,7 +56,6 @@ public class SynchronizerFactory : ISynchronizerFactory
|
|||||||
_calDavClient = calDavClient;
|
_calDavClient = calDavClient;
|
||||||
_autoDiscoveryService = autoDiscoveryService;
|
_autoDiscoveryService = autoDiscoveryService;
|
||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId)
|
public async Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId)
|
||||||
@@ -89,7 +86,7 @@ public class SynchronizerFactory : ISynchronizerFactory
|
|||||||
{
|
{
|
||||||
case Domain.Enums.MailProviderType.Outlook:
|
case Domain.Enums.MailProviderType.Outlook:
|
||||||
var outlookAuthenticator = _authenticationProvider.GetAuthenticator(Domain.Enums.MailProviderType.Outlook) as IOutlookAuthenticator;
|
var outlookAuthenticator = _authenticationProvider.GetAuthenticator(Domain.Enums.MailProviderType.Outlook) as IOutlookAuthenticator;
|
||||||
return new OutlookSynchronizer(mailAccount, outlookAuthenticator, _outlookChangeProcessor, _outlookSynchronizerErrorHandlerFactory, _mailCategoryService);
|
return new OutlookSynchronizer(mailAccount, outlookAuthenticator, _outlookChangeProcessor, _outlookSynchronizerErrorHandlerFactory);
|
||||||
case Domain.Enums.MailProviderType.Gmail:
|
case Domain.Enums.MailProviderType.Gmail:
|
||||||
var gmailAuthenticator = _authenticationProvider.GetAuthenticator(Domain.Enums.MailProviderType.Gmail) as IGmailAuthenticator;
|
var gmailAuthenticator = _authenticationProvider.GetAuthenticator(Domain.Enums.MailProviderType.Gmail) as IGmailAuthenticator;
|
||||||
return new GmailSynchronizer(mailAccount, gmailAuthenticator, _gmailChangeProcessor, _gmailSynchronizerErrorHandlerFactory);
|
return new GmailSynchronizer(mailAccount, gmailAuthenticator, _gmailChangeProcessor, _gmailSynchronizerErrorHandlerFactory);
|
||||||
|
|||||||
@@ -165,13 +165,6 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
|||||||
if (calendarPreparationRequest == null)
|
if (calendarPreparationRequest == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var resolvedCalendar = await ResolveCalendarAsync(calendarPreparationRequest).ConfigureAwait(false);
|
|
||||||
if (resolvedCalendar?.IsReadOnly == true)
|
|
||||||
{
|
|
||||||
_dialogService.ShowReadOnlyCalendarMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IRequestBase request = calendarPreparationRequest.Operation switch
|
IRequestBase request = calendarPreparationRequest.Operation switch
|
||||||
{
|
{
|
||||||
CalendarSynchronizerOperation.CreateEvent => await CreateCalendarEventRequestAsync(calendarPreparationRequest).ConfigureAwait(false),
|
CalendarSynchronizerOperation.CreateEvent => await CreateCalendarEventRequestAsync(calendarPreparationRequest).ConfigureAwait(false),
|
||||||
@@ -207,21 +200,6 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
|||||||
await QueueCalendarSynchronizationAsync(accountId);
|
await QueueCalendarSynchronizationAsync(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteAsync(Guid accountId, IEnumerable<IRequestBase> requests)
|
|
||||||
{
|
|
||||||
var requestList = requests?.Where(a => a != null).ToList() ?? [];
|
|
||||||
if (requestList.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var request in requestList)
|
|
||||||
{
|
|
||||||
await QueueRequestAsync(request, accountId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await SendSyncActionsAddedAsync(requestList, accountId).ConfigureAwait(false);
|
|
||||||
await QueueSynchronizationAsync(accountId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IRequestBase> CreateCalendarEventRequestAsync(CalendarOperationPreparationRequest calendarPreparationRequest)
|
private async Task<IRequestBase> CreateCalendarEventRequestAsync(CalendarOperationPreparationRequest calendarPreparationRequest)
|
||||||
{
|
{
|
||||||
var composeResult = calendarPreparationRequest.ComposeResult
|
var composeResult = calendarPreparationRequest.ComposeResult
|
||||||
@@ -234,25 +212,6 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
|||||||
return new CreateCalendarEventRequest(composeResult, assignedCalendar);
|
return new CreateCalendarEventRequest(composeResult, assignedCalendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<AccountCalendar> ResolveCalendarAsync(CalendarOperationPreparationRequest calendarPreparationRequest)
|
|
||||||
{
|
|
||||||
if (calendarPreparationRequest.Operation == CalendarSynchronizerOperation.CreateEvent)
|
|
||||||
{
|
|
||||||
var calendarId = calendarPreparationRequest.ComposeResult?.CalendarId ?? Guid.Empty;
|
|
||||||
return calendarId == Guid.Empty
|
|
||||||
? null
|
|
||||||
: await _calendarService.GetAccountCalendarAsync(calendarId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calendarPreparationRequest.CalendarItem?.AssignedCalendar is AccountCalendar assignedCalendar)
|
|
||||||
return assignedCalendar;
|
|
||||||
|
|
||||||
var fallbackCalendarId = calendarPreparationRequest.CalendarItem?.CalendarId ?? Guid.Empty;
|
|
||||||
return fallbackCalendarId == Guid.Empty
|
|
||||||
? null
|
|
||||||
: await _calendarService.GetAccountCalendarAsync(fallbackCalendarId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRequestBase CreateDeclineRequest(CalendarItem calendarItem, string responseMessage)
|
private IRequestBase CreateDeclineRequest(CalendarItem calendarItem, string responseMessage)
|
||||||
{
|
{
|
||||||
// For Outlook accounts, declined events are deleted by the server after synchronization.
|
// For Outlook accounts, declined events are deleted by the server after synchronization.
|
||||||
|
|||||||
@@ -759,8 +759,6 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
existingLocalCalendar.BackgroundColorHex = resolvedColor;
|
existingLocalCalendar.BackgroundColorHex = resolvedColor;
|
||||||
existingLocalCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocalCalendar.BackgroundColorHex);
|
existingLocalCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocalCalendar.BackgroundColorHex);
|
||||||
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
existingLocalCalendar.IsReadOnly = !string.Equals(calendar.AccessRole, "owner", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.Equals(calendar.AccessRole, "writer", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
updatedCalendars.Add(existingLocalCalendar);
|
updatedCalendars.Add(existingLocalCalendar);
|
||||||
}
|
}
|
||||||
@@ -942,17 +940,14 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
var remoteBackgroundColor = ResolveSynchronizedCalendarBackgroundColor(GetRemoteGmailCalendarBackgroundColor(calendarListEntry), accountCalendar);
|
var remoteBackgroundColor = ResolveSynchronizedCalendarBackgroundColor(GetRemoteGmailCalendarBackgroundColor(calendarListEntry), accountCalendar);
|
||||||
var remoteTextColor = ColorHelpers.GetReadableTextColorHex(remoteBackgroundColor);
|
var remoteTextColor = ColorHelpers.GetReadableTextColorHex(remoteBackgroundColor);
|
||||||
var remoteIsPrimary = string.Equals(calendarListEntry.Id, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
var remoteIsPrimary = string.Equals(calendarListEntry.Id, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
var remoteIsReadOnly = !string.Equals(calendarListEntry.AccessRole, "owner", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.Equals(calendarListEntry.AccessRole, "writer", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
bool isNameChanged = !string.Equals(accountCalendar.Name, remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
bool isNameChanged = !string.Equals(accountCalendar.Name, remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
||||||
bool isTimeZoneChanged = !string.Equals(accountCalendar.TimeZone, remoteTimeZone, StringComparison.OrdinalIgnoreCase);
|
bool isTimeZoneChanged = !string.Equals(accountCalendar.TimeZone, remoteTimeZone, StringComparison.OrdinalIgnoreCase);
|
||||||
bool isBackgroundColorChanged = !string.Equals(accountCalendar.BackgroundColorHex, remoteBackgroundColor, StringComparison.OrdinalIgnoreCase);
|
bool isBackgroundColorChanged = !string.Equals(accountCalendar.BackgroundColorHex, remoteBackgroundColor, StringComparison.OrdinalIgnoreCase);
|
||||||
bool isTextColorChanged = !string.Equals(accountCalendar.TextColorHex, remoteTextColor, StringComparison.OrdinalIgnoreCase);
|
bool isTextColorChanged = !string.Equals(accountCalendar.TextColorHex, remoteTextColor, StringComparison.OrdinalIgnoreCase);
|
||||||
bool isPrimaryChanged = accountCalendar.IsPrimary != remoteIsPrimary;
|
bool isPrimaryChanged = accountCalendar.IsPrimary != remoteIsPrimary;
|
||||||
bool isReadOnlyChanged = accountCalendar.IsReadOnly != remoteIsReadOnly;
|
|
||||||
|
|
||||||
return isNameChanged || isTimeZoneChanged || isBackgroundColorChanged || isTextColorChanged || isPrimaryChanged || isReadOnlyChanged;
|
return isNameChanged || isTimeZoneChanged || isBackgroundColorChanged || isTextColorChanged || isPrimaryChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetRemoteGmailCalendarBackgroundColor(CalendarListEntry calendarListEntry)
|
private static string GetRemoteGmailCalendarBackgroundColor(CalendarListEntry calendarListEntry)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ using Wino.Core.Integration.Processors;
|
|||||||
using Wino.Core.Misc;
|
using Wino.Core.Misc;
|
||||||
using Wino.Core.Requests.Bundles;
|
using Wino.Core.Requests.Bundles;
|
||||||
using Wino.Core.Requests.Calendar;
|
using Wino.Core.Requests.Calendar;
|
||||||
using Wino.Core.Requests.Category;
|
|
||||||
using Wino.Core.Requests.Folder;
|
using Wino.Core.Requests.Folder;
|
||||||
using Wino.Core.Requests.Mail;
|
using Wino.Core.Requests.Mail;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
@@ -108,7 +107,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
"ParentFolderId",
|
"ParentFolderId",
|
||||||
"InternetMessageId",
|
"InternetMessageId",
|
||||||
"InternetMessageHeaders",
|
"InternetMessageHeaders",
|
||||||
"Categories",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly SemaphoreSlim _handleItemRetrievalSemaphore = new(1);
|
private readonly SemaphoreSlim _handleItemRetrievalSemaphore = new(1);
|
||||||
@@ -118,7 +116,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
||||||
private readonly GraphServiceClient _graphClient;
|
private readonly GraphServiceClient _graphClient;
|
||||||
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
|
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
private bool _isFolderStructureChanged;
|
private bool _isFolderStructureChanged;
|
||||||
|
|
||||||
private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads
|
private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads
|
||||||
@@ -126,8 +123,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
public OutlookSynchronizer(MailAccount account,
|
public OutlookSynchronizer(MailAccount account,
|
||||||
IAuthenticator authenticator,
|
IAuthenticator authenticator,
|
||||||
IOutlookChangeProcessor outlookChangeProcessor,
|
IOutlookChangeProcessor outlookChangeProcessor,
|
||||||
IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory,
|
IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory) : base(account, WeakReferenceMessenger.Default)
|
||||||
IMailCategoryService mailCategoryService) : base(account, WeakReferenceMessenger.Default)
|
|
||||||
{
|
{
|
||||||
var tokenProvider = new MicrosoftTokenProvider(Account, authenticator);
|
var tokenProvider = new MicrosoftTokenProvider(Account, authenticator);
|
||||||
|
|
||||||
@@ -142,7 +138,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
_outlookChangeProcessor = outlookChangeProcessor;
|
_outlookChangeProcessor = outlookChangeProcessor;
|
||||||
_errorHandlingFactory = errorHandlingFactory;
|
_errorHandlingFactory = errorHandlingFactory;
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region MS Graph Handlers
|
#region MS Graph Handlers
|
||||||
@@ -1157,11 +1152,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
_logger.Debug("Updating flag status for mail {MessageId}: IsFlagged={IsFlagged}", item.Id, isFlagged);
|
_logger.Debug("Updating flag status for mail {MessageId}: IsFlagged={IsFlagged}", item.Id, isFlagged);
|
||||||
await _outlookChangeProcessor.ChangeFlagStatusAsync(item.Id, isFlagged).ConfigureAwait(false);
|
await _outlookChangeProcessor.ChangeFlagStatusAsync(item.Id, isFlagged).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Categories != null)
|
|
||||||
{
|
|
||||||
await ReplaceMailAssignmentsAsync(item.Id, item.Categories).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1218,43 +1208,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task SynchronizeCategoriesAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var response = await _graphClient.Me.Outlook.MasterCategories
|
|
||||||
.GetAsync(cancellationToken: cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var categories = response?.Value?
|
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a?.DisplayName))
|
|
||||||
.Select(a =>
|
|
||||||
{
|
|
||||||
var colorOption = GetMailCategoryColorOption(a.Color);
|
|
||||||
|
|
||||||
return new MailCategory
|
|
||||||
{
|
|
||||||
MailAccountId = Account.Id,
|
|
||||||
RemoteId = a.Id,
|
|
||||||
Name = a.DisplayName,
|
|
||||||
BackgroundColorHex = colorOption.BackgroundColorHex,
|
|
||||||
TextColorHex = colorOption.TextColorHex,
|
|
||||||
Source = MailCategorySource.Outlook
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ToList() ?? [];
|
|
||||||
|
|
||||||
await _mailCategoryService.ReplaceCategoriesAsync(Account.Id, categories).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReplaceMailAssignmentsAsync(string messageId, IEnumerable<string> categoryNames)
|
|
||||||
{
|
|
||||||
var localMailCopies = await _outlookChangeProcessor.GetMailCopiesAsync([messageId]).ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var localMailCopy in localMailCopies)
|
|
||||||
{
|
|
||||||
await _mailCategoryService.ReplaceMailAssignmentsAsync(Account.Id, localMailCopy.UniqueId, categoryNames ?? []).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<OutlookSpecialFolderIdInformation> GetSpecialFolderIdsAsync(CancellationToken cancellationToken)
|
private async Task<OutlookSpecialFolderIdInformation> GetSpecialFolderIdsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var localFolders = await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
var localFolders = await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||||
@@ -1814,87 +1767,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
return Move(batchMoveRequest);
|
return Move(batchMoveRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<IRequestBundle<RequestInformation>> UpdateCategories(BatchMailCategoryAssignmentRequest request)
|
|
||||||
=> ForEachRequest(request, item => CreateMessageCategoryPatchRequest(item.Item.Id, item.CategoryNames));
|
|
||||||
|
|
||||||
public override List<IRequestBundle<RequestInformation>> CreateCategory(MailCategoryCreateRequest request)
|
|
||||||
{
|
|
||||||
var outlookCategory = new OutlookCategory
|
|
||||||
{
|
|
||||||
DisplayName = request.Category.Name,
|
|
||||||
Color = GetOutlookCategoryColor(request.Category)
|
|
||||||
};
|
|
||||||
|
|
||||||
var requestInfo = _graphClient.Me.Outlook.MasterCategories.ToPostRequestInformation(outlookCategory);
|
|
||||||
return [new HttpRequestBundle<RequestInformation>(requestInfo, request)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public override List<IRequestBundle<RequestInformation>> UpdateCategory(MailCategoryUpdateRequest request)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(request.PreviousRemoteId))
|
|
||||||
return CreateCategory(new MailCategoryCreateRequest(request.Category));
|
|
||||||
|
|
||||||
var hasNameChanged = !string.Equals(request.PreviousName, request.Category.Name, StringComparison.Ordinal);
|
|
||||||
if (!hasNameChanged)
|
|
||||||
{
|
|
||||||
var requestInfo = _graphClient.Me.Outlook.MasterCategories[request.PreviousRemoteId].ToPatchRequestInformation(new OutlookCategory
|
|
||||||
{
|
|
||||||
Color = GetOutlookCategoryColor(request.Category)
|
|
||||||
});
|
|
||||||
|
|
||||||
return [new HttpRequestBundle<RequestInformation>(requestInfo, request)];
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundles = new List<IRequestBundle<RequestInformation>>();
|
|
||||||
var createRequestInfo = _graphClient.Me.Outlook.MasterCategories.ToPostRequestInformation(new OutlookCategory
|
|
||||||
{
|
|
||||||
DisplayName = request.Category.Name,
|
|
||||||
Color = GetOutlookCategoryColor(request.Category)
|
|
||||||
});
|
|
||||||
|
|
||||||
bundles.Add(new HttpRequestBundle<RequestInformation>(createRequestInfo, request));
|
|
||||||
|
|
||||||
foreach (var target in request.AffectedMessages ?? [])
|
|
||||||
{
|
|
||||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
|
||||||
CreateMessageCategoryPatchRequest(target.MessageId, target.CategoryNames),
|
|
||||||
request));
|
|
||||||
}
|
|
||||||
|
|
||||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
|
||||||
_graphClient.Me.Outlook.MasterCategories[request.PreviousRemoteId].ToDeleteRequestInformation(),
|
|
||||||
request));
|
|
||||||
|
|
||||||
return bundles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override List<IRequestBundle<RequestInformation>> DeleteCategory(MailCategoryDeleteRequest request)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(request.PreviousRemoteId))
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var bundles = new List<IRequestBundle<RequestInformation>>();
|
|
||||||
|
|
||||||
foreach (var target in request.AffectedMessages ?? [])
|
|
||||||
{
|
|
||||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
|
||||||
CreateMessageCategoryPatchRequest(target.MessageId, target.CategoryNames),
|
|
||||||
request));
|
|
||||||
}
|
|
||||||
|
|
||||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
|
||||||
_graphClient.Me.Outlook.MasterCategories[request.PreviousRemoteId].ToDeleteRequestInformation(),
|
|
||||||
request));
|
|
||||||
|
|
||||||
return bundles;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestInformation CreateMessageCategoryPatchRequest(string messageId, IReadOnlyList<string> categoryNames)
|
|
||||||
=> _graphClient.Me.Messages[messageId].ToPatchRequestInformation(new Message
|
|
||||||
{
|
|
||||||
Categories = categoryNames?.ToList() ?? []
|
|
||||||
});
|
|
||||||
|
|
||||||
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
|
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
|
||||||
MailKit.ITransferProgress transferProgress = null,
|
MailKit.ITransferProgress transferProgress = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -2090,9 +1962,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
for (int i = 0; i < itemCount; i++)
|
for (int i = 0; i < itemCount; i++)
|
||||||
{
|
{
|
||||||
var bundle = batch.ElementAt(i);
|
var bundle = batch.ElementAt(i);
|
||||||
requiresSerial |= bundle.UIChangeRequest is SendDraftRequest
|
requiresSerial |= bundle.UIChangeRequest is SendDraftRequest;
|
||||||
or MailCategoryUpdateRequest
|
|
||||||
or MailCategoryDeleteRequest;
|
|
||||||
|
|
||||||
// UI changes are already applied in ExecuteNativeRequestsAsync before batching.
|
// UI changes are already applied in ExecuteNativeRequestsAsync before batching.
|
||||||
var batchRequestId = await batchContent.AddBatchRequestStepAsync(bundle.NativeRequest);
|
var batchRequestId = await batchContent.AddBatchRequestStepAsync(bundle.NativeRequest);
|
||||||
@@ -2240,10 +2110,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|| request is ChangeFlagRequest
|
|| request is ChangeFlagRequest
|
||||||
|| request is MarkReadRequest
|
|| request is MarkReadRequest
|
||||||
|| request is ArchiveRequest
|
|| request is ArchiveRequest
|
||||||
|| request is MailCategoryAssignmentRequest
|
|
||||||
|| request is RenameFolderRequest
|
|| request is RenameFolderRequest
|
||||||
|| request is MailCategoryUpdateRequest
|
|
||||||
|| request is MailCategoryDeleteRequest
|
|
||||||
|| request is DeleteFolderRequest
|
|| request is DeleteFolderRequest
|
||||||
|| request is AcceptEventRequest
|
|| request is AcceptEventRequest
|
||||||
|| request is DeclineEventRequest
|
|| request is DeclineEventRequest
|
||||||
@@ -2298,26 +2165,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
await UploadCalendarEventAttachmentsAsync(createCalendarEventRequest, createdEventId, CancellationToken.None).ConfigureAwait(false);
|
await UploadCalendarEventAttachmentsAsync(createCalendarEventRequest, createdEventId, CancellationToken.None).ConfigureAwait(false);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bundle?.UIChangeRequest is MailCategoryCreateRequest createCategoryRequest)
|
|
||||||
{
|
|
||||||
var createdCategoryId = json?["id"]?.GetValue<string>();
|
|
||||||
if (!string.IsNullOrWhiteSpace(createdCategoryId))
|
|
||||||
{
|
|
||||||
await _mailCategoryService.UpdateRemoteIdAsync(createCategoryRequest.Category.Id, createdCategoryId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bundle?.UIChangeRequest is MailCategoryUpdateRequest updateCategoryRequest)
|
|
||||||
{
|
|
||||||
var updatedCategoryId = json?["id"]?.GetValue<string>();
|
|
||||||
if (!string.IsNullOrWhiteSpace(updatedCategoryId))
|
|
||||||
{
|
|
||||||
await _mailCategoryService.UpdateRemoteIdAsync(updateCategoryRequest.Category.Id, updatedCategoryId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -2520,68 +2367,11 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
// Outlook messages can only be assigned to 1 folder at a time.
|
// Outlook messages can only be assigned to 1 folder at a time.
|
||||||
// Therefore we don't need to create multiple copies of the same message for different folders.
|
// Therefore we don't need to create multiple copies of the same message for different folders.
|
||||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||||
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts, message.Categories);
|
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts);
|
||||||
|
|
||||||
return [package];
|
return [package];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MailCategoryColorOption GetMailCategoryColorOption(CategoryColor? color)
|
|
||||||
=> color switch
|
|
||||||
{
|
|
||||||
CategoryColor.Preset0 => new("#FEE2E2", "#991B1B"),
|
|
||||||
CategoryColor.Preset1 => new("#FFEDD5", "#9A3412"),
|
|
||||||
CategoryColor.Preset2 => new("#FEF3C7", "#92400E"),
|
|
||||||
CategoryColor.Preset3 => new("#ECFCCB", "#3F6212"),
|
|
||||||
CategoryColor.Preset4 => new("#DCFCE7", "#166534"),
|
|
||||||
CategoryColor.Preset5 => new("#CCFBF1", "#115E59"),
|
|
||||||
CategoryColor.Preset6 => new("#CFFAFE", "#155E75"),
|
|
||||||
CategoryColor.Preset7 => new("#DBEAFE", "#1D4ED8"),
|
|
||||||
CategoryColor.Preset8 => new("#E0E7FF", "#4338CA"),
|
|
||||||
CategoryColor.Preset9 => new("#F3E8FF", "#7E22CE"),
|
|
||||||
CategoryColor.Preset10 => new("#FCE7F3", "#9D174D"),
|
|
||||||
CategoryColor.Preset11 => new("#FECACA", "#7F1D1D"),
|
|
||||||
CategoryColor.Preset12 => new("#FED7AA", "#7C2D12"),
|
|
||||||
CategoryColor.Preset13 => new("#FDE68A", "#78350F"),
|
|
||||||
CategoryColor.Preset14 => new("#D9F99D", "#365314"),
|
|
||||||
CategoryColor.Preset15 => new("#BBF7D0", "#14532D"),
|
|
||||||
CategoryColor.Preset16 => new("#99F6E4", "#134E4A"),
|
|
||||||
CategoryColor.Preset17 => new("#A5F3FC", "#164E63"),
|
|
||||||
CategoryColor.Preset18 => new("#BFDBFE", "#1E3A8A"),
|
|
||||||
CategoryColor.Preset19 => new("#DDD6FE", "#5B21B6"),
|
|
||||||
CategoryColor.Preset20 => new("#E5E7EB", "#374151"),
|
|
||||||
CategoryColor.Preset21 => new("#D1D5DB", "#1F2937"),
|
|
||||||
CategoryColor.Preset22 => new("#F3F4F6", "#111827"),
|
|
||||||
CategoryColor.Preset23 => new("#E2E8F0", "#334155"),
|
|
||||||
CategoryColor.Preset24 => new("#F8FAFC", "#475569"),
|
|
||||||
_ => new("#E5E7EB", "#374151")
|
|
||||||
};
|
|
||||||
|
|
||||||
private static CategoryColor GetOutlookCategoryColor(MailCategory category)
|
|
||||||
=> (category.BackgroundColorHex?.ToUpperInvariant(), category.TextColorHex?.ToUpperInvariant()) switch
|
|
||||||
{
|
|
||||||
("#FEE2E2", "#991B1B") => CategoryColor.Preset0,
|
|
||||||
("#FFEDD5", "#9A3412") => CategoryColor.Preset1,
|
|
||||||
("#FEF3C7", "#92400E") => CategoryColor.Preset2,
|
|
||||||
("#ECFCCB", "#3F6212") => CategoryColor.Preset3,
|
|
||||||
("#DCFCE7", "#166534") => CategoryColor.Preset4,
|
|
||||||
("#CCFBF1", "#115E59") => CategoryColor.Preset5,
|
|
||||||
("#CFFAFE", "#155E75") => CategoryColor.Preset6,
|
|
||||||
("#DBEAFE", "#1D4ED8") => CategoryColor.Preset7,
|
|
||||||
("#E0E7FF", "#4338CA") => CategoryColor.Preset8,
|
|
||||||
("#F3E8FF", "#7E22CE") => CategoryColor.Preset9,
|
|
||||||
("#FCE7F3", "#9D174D") => CategoryColor.Preset10,
|
|
||||||
("#FECACA", "#7F1D1D") => CategoryColor.Preset11,
|
|
||||||
("#FED7AA", "#7C2D12") => CategoryColor.Preset12,
|
|
||||||
("#FDE68A", "#78350F") => CategoryColor.Preset13,
|
|
||||||
("#D9F99D", "#365314") => CategoryColor.Preset14,
|
|
||||||
("#BBF7D0", "#14532D") => CategoryColor.Preset15,
|
|
||||||
("#99F6E4", "#134E4A") => CategoryColor.Preset16,
|
|
||||||
("#A5F3FC", "#164E63") => CategoryColor.Preset17,
|
|
||||||
("#BFDBFE", "#1E3A8A") => CategoryColor.Preset18,
|
|
||||||
("#DDD6FE", "#5B21B6") => CategoryColor.Preset19,
|
|
||||||
_ => CategoryColor.Preset0
|
|
||||||
};
|
|
||||||
|
|
||||||
private async Task TryMapCalendarInvitationAsync(MailCopy mailCopy, MimeMessage mimeMessage, CancellationToken cancellationToken)
|
private async Task TryMapCalendarInvitationAsync(MailCopy mailCopy, MimeMessage mimeMessage, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (mailCopy.ItemType != MailItemType.CalendarInvitation || mimeMessage == null)
|
if (mailCopy.ItemType != MailItemType.CalendarInvitation || mimeMessage == null)
|
||||||
@@ -2884,7 +2674,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
{
|
{
|
||||||
existingLocalCalendar.Name = calendar.Name;
|
existingLocalCalendar.Name = calendar.Name;
|
||||||
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
existingLocalCalendar.IsReadOnly = !calendar.CanEdit.GetValueOrDefault(true);
|
|
||||||
existingLocalCalendar.BackgroundColorHex = resolvedColor;
|
existingLocalCalendar.BackgroundColorHex = resolvedColor;
|
||||||
existingLocalCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocalCalendar.BackgroundColorHex);
|
existingLocalCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocalCalendar.BackgroundColorHex);
|
||||||
|
|
||||||
@@ -2923,14 +2712,12 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
var remoteCalendarName = calendar.Name;
|
var remoteCalendarName = calendar.Name;
|
||||||
var remoteBackgroundColor = ResolveSynchronizedCalendarBackgroundColor(GetRemoteOutlookCalendarBackgroundColor(calendar), accountCalendar);
|
var remoteBackgroundColor = ResolveSynchronizedCalendarBackgroundColor(GetRemoteOutlookCalendarBackgroundColor(calendar), accountCalendar);
|
||||||
var remoteIsPrimary = string.Equals(calendar.Id, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
var remoteIsPrimary = string.Equals(calendar.Id, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
var remoteIsReadOnly = !calendar.CanEdit.GetValueOrDefault(true);
|
|
||||||
|
|
||||||
bool isNameChanged = !string.Equals(accountCalendar.Name, remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
bool isNameChanged = !string.Equals(accountCalendar.Name, remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
||||||
bool isBackgroundColorChanged = !string.Equals(accountCalendar.BackgroundColorHex, remoteBackgroundColor, StringComparison.OrdinalIgnoreCase);
|
bool isBackgroundColorChanged = !string.Equals(accountCalendar.BackgroundColorHex, remoteBackgroundColor, StringComparison.OrdinalIgnoreCase);
|
||||||
bool isPrimaryChanged = accountCalendar.IsPrimary != remoteIsPrimary;
|
bool isPrimaryChanged = accountCalendar.IsPrimary != remoteIsPrimary;
|
||||||
bool isReadOnlyChanged = accountCalendar.IsReadOnly != remoteIsReadOnly;
|
|
||||||
|
|
||||||
return isNameChanged || isBackgroundColorChanged || isPrimaryChanged || isReadOnlyChanged;
|
return isNameChanged || isBackgroundColorChanged || isPrimaryChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetRemoteOutlookCalendarBackgroundColor(Calendar calendar)
|
private static string GetRemoteOutlookCalendarBackgroundColor(Calendar calendar)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ using Wino.Core.Domain.Models.MailItem;
|
|||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.Requests.Bundles;
|
using Wino.Core.Requests.Bundles;
|
||||||
using Wino.Core.Requests.Calendar;
|
using Wino.Core.Requests.Calendar;
|
||||||
using Wino.Core.Requests.Category;
|
|
||||||
using Wino.Core.Requests.Folder;
|
using Wino.Core.Requests.Folder;
|
||||||
using Wino.Core.Requests.Mail;
|
using Wino.Core.Requests.Mail;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
@@ -64,7 +63,6 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
|||||||
/// Only available for Gmail right now.
|
/// Only available for Gmail right now.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
|
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
|
||||||
protected virtual Task SynchronizeCategoriesAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queues all mail ids for initial synchronization for a specific folder.
|
/// Queues all mail ids for initial synchronization for a specific folder.
|
||||||
@@ -196,9 +194,6 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
|||||||
case MailSynchronizerOperation.Archive:
|
case MailSynchronizerOperation.Archive:
|
||||||
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
|
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
|
||||||
break;
|
break;
|
||||||
case MailSynchronizerOperation.UpdateCategories:
|
|
||||||
nativeRequests.AddRange(UpdateCategories(new BatchMailCategoryAssignmentRequest(group.Cast<MailCategoryAssignmentRequest>())));
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -226,23 +221,6 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (key is CategorySynchronizerOperation categorySynchronizerOperation)
|
|
||||||
{
|
|
||||||
switch (categorySynchronizerOperation)
|
|
||||||
{
|
|
||||||
case CategorySynchronizerOperation.CreateCategory:
|
|
||||||
nativeRequests.AddRange(CreateCategory(group.ElementAt(0) as MailCategoryCreateRequest));
|
|
||||||
break;
|
|
||||||
case CategorySynchronizerOperation.UpdateCategory:
|
|
||||||
nativeRequests.AddRange(UpdateCategory(group.ElementAt(0) as MailCategoryUpdateRequest));
|
|
||||||
break;
|
|
||||||
case CategorySynchronizerOperation.DeleteCategory:
|
|
||||||
nativeRequests.AddRange(DeleteCategory(group.ElementAt(0) as MailCategoryDeleteRequest));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeRequestQueue.Clear();
|
changeRequestQueue.Clear();
|
||||||
@@ -344,30 +322,6 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category definition sync.
|
|
||||||
if (options.Type == MailSynchronizationType.Categories)
|
|
||||||
{
|
|
||||||
if (!Account.IsCategorySyncSupported) return MailSynchronizationResult.Empty;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await SynchronizeCategoriesAsync(activeSynchronizationCancellationToken);
|
|
||||||
|
|
||||||
return FinalizeMailResult(MailSynchronizationResult.Empty);
|
|
||||||
}
|
|
||||||
catch (AuthenticationAttentionException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Failed to update categories for {Name}", Account.Name);
|
|
||||||
|
|
||||||
CaptureSynchronizationIssue(SynchronizationIssue.FromException(ex, "CategorySync"));
|
|
||||||
return FinalizeMailResult(MailSynchronizationResult.Failed(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldDelayExecution)
|
if (shouldDelayExecution)
|
||||||
{
|
{
|
||||||
await Task.Delay(maxExecutionDelay);
|
await Task.Delay(maxExecutionDelay);
|
||||||
@@ -572,16 +526,6 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
|||||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||||
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
|
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
|
||||||
{
|
{
|
||||||
if (requests.All(a => a is ICategoryActionRequest or MailCategoryAssignmentRequest))
|
|
||||||
{
|
|
||||||
return new MailSynchronizationOptions
|
|
||||||
{
|
|
||||||
AccountId = Account.Id,
|
|
||||||
Id = existingSynchronizationId,
|
|
||||||
Type = MailSynchronizationType.FoldersOnly
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Guid> synchronizationFolderIds = requests
|
List<Guid> synchronizationFolderIds = requests
|
||||||
.Where(a => a is ICustomFolderSynchronizationRequest)
|
.Where(a => a is ICustomFolderSynchronizationRequest)
|
||||||
.Cast<ICustomFolderSynchronizationRequest>()
|
.Cast<ICustomFolderSynchronizationRequest>()
|
||||||
@@ -658,10 +602,6 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
|||||||
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||||
public virtual List<IRequestBundle<TBaseRequest>> DeleteFolder(DeleteFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
public virtual List<IRequestBundle<TBaseRequest>> DeleteFolder(DeleteFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||||
public virtual List<IRequestBundle<TBaseRequest>> CreateSubFolder(CreateSubFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
public virtual List<IRequestBundle<TBaseRequest>> CreateSubFolder(CreateSubFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||||
public virtual List<IRequestBundle<TBaseRequest>> UpdateCategories(BatchMailCategoryAssignmentRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
|
||||||
public virtual List<IRequestBundle<TBaseRequest>> CreateCategory(MailCategoryCreateRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
|
||||||
public virtual List<IRequestBundle<TBaseRequest>> UpdateCategory(MailCategoryUpdateRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
|
||||||
public virtual List<IRequestBundle<TBaseRequest>> DeleteCategory(MailCategoryDeleteRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@@ -169,10 +169,6 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
private void EditAliases()
|
private void EditAliases()
|
||||||
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
|
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void EditCategories()
|
|
||||||
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.MailCategoryManagementPage_Title, WinoPage.MailCategoryManagementPage, Account.Id));
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void EditImapCalDavSettings()
|
private void EditImapCalDavSettings()
|
||||||
=> Messenger.Send(new BreadcrumbNavigationRequested(
|
=> Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
|||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
|
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
|
||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
|
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
|
||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
|
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
|
||||||
if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook)
|
|
||||||
{
|
|
||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories });
|
|
||||||
}
|
|
||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
|
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
|
||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
|
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
|
||||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
|
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
|
||||||
@@ -233,16 +229,6 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
|||||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
||||||
SetCurrentStepSucceeded();
|
SetCurrentStepSucceeded();
|
||||||
|
|
||||||
// Step: Categories
|
|
||||||
if (_createdAccount.IsCategorySyncSupported)
|
|
||||||
{
|
|
||||||
SetStepInProgress(Translator.AccountSetup_Step_SyncingCategories);
|
|
||||||
var categoryResult = await SynchronizationManager.Instance.SynchronizeCategoriesAsync(_createdAccount.Id);
|
|
||||||
if (categoryResult.CompletedState != SynchronizationCompletedState.Success)
|
|
||||||
throw new Exception(Translator.Exception_FailedToSynchronizeCategories);
|
|
||||||
SetCurrentStepSucceeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step: Calendar metadata
|
// Step: Calendar metadata
|
||||||
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
|
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
|
||||||
if (_createdAccount.IsCalendarAccessGranted)
|
if (_createdAccount.IsCalendarAccessGranted)
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
public IMenuItem CreatePrimaryMenuItem => CreateMailMenuItem;
|
public IMenuItem CreatePrimaryMenuItem => CreateMailMenuItem;
|
||||||
|
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
private readonly IConfigurationService _configurationService;
|
private readonly IConfigurationService _configurationService;
|
||||||
private readonly IStartupBehaviorService _startupBehaviorService;
|
private readonly IStartupBehaviorService _startupBehaviorService;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
@@ -100,7 +99,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
IMimeFileService mimeFileService,
|
IMimeFileService mimeFileService,
|
||||||
INativeAppService nativeAppService,
|
INativeAppService nativeAppService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IMailCategoryService mailCategoryService,
|
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
IContextMenuItemService contextMenuItemService,
|
IContextMenuItemService contextMenuItemService,
|
||||||
IStoreRatingService storeRatingService,
|
IStoreRatingService storeRatingService,
|
||||||
@@ -127,7 +125,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
_mimeFileService = mimeFileService;
|
_mimeFileService = mimeFileService;
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
_folderService = folderService;
|
_folderService = folderService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_contextMenuItemService = contextMenuItemService;
|
_contextMenuItemService = contextMenuItemService;
|
||||||
@@ -724,8 +721,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
{
|
{
|
||||||
await HandleCreateNewMailAsync();
|
await HandleCreateNewMailAsync();
|
||||||
}
|
}
|
||||||
else if (clickedMenuItem is IBaseFolderMenuItem baseFolderMenuItem &&
|
else if (clickedMenuItem is IBaseFolderMenuItem baseFolderMenuItem && baseFolderMenuItem.HandlingFolders.All(a => a.IsMoveTarget))
|
||||||
(clickedMenuItem is IMailCategoryMenuItem or IMergedMailCategoryMenuItem || baseFolderMenuItem.HandlingFolders.All(a => a.IsMoveTarget)))
|
|
||||||
{
|
{
|
||||||
// Don't navigate to base folders that contain non-move target folders.
|
// Don't navigate to base folders that contain non-move target folders.
|
||||||
// Theory: This is a special folder like Categories or More. Don't navigate to it.
|
// Theory: This is a special folder like Categories or More. Don't navigate to it.
|
||||||
@@ -797,20 +793,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
{
|
{
|
||||||
// Get visible account menu items, ordered by merged accounts at the last.
|
// Get visible account menu items, ordered by merged accounts at the last.
|
||||||
// We will update the unread counts for all single accounts and trigger UI refresh for merged menu items.
|
// We will update the unread counts for all single accounts and trigger UI refresh for merged menu items.
|
||||||
List<IAccountMenuItem> accountMenuItems = null;
|
var accountMenuItems = MenuItems.GetAllAccountMenuItems().OrderBy(a => a.HoldingAccounts.Count());
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
|
||||||
{
|
|
||||||
accountMenuItems = MenuItems
|
|
||||||
.GetAllAccountMenuItems()
|
|
||||||
.OrderBy(a => a.HoldingAccounts.Count())
|
|
||||||
.ToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Individually get all single accounts' unread counts.
|
// Individually get all single accounts' unread counts.
|
||||||
var accountIds = accountMenuItems.OfType<AccountMenuItem>().Select(a => a.AccountId).ToList();
|
var accountIds = accountMenuItems.OfType<AccountMenuItem>().Select(a => a.AccountId);
|
||||||
var unreadCountResult = await _folderService.GetUnreadItemCountResultsAsync(accountIds).ConfigureAwait(false);
|
var unreadCountResult = await _folderService.GetUnreadItemCountResultsAsync(accountIds).ConfigureAwait(false);
|
||||||
var unreadCategoryCountResult = await _mailCategoryService.GetUnreadCategoryCountResultsAsync(accountIds).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Recursively update all folders' unread counts to 0.
|
// Recursively update all folders' unread counts to 0.
|
||||||
// Query above only returns unread counts that exists. We need to reset the rest to 0 first.
|
// Query above only returns unread counts that exists. We need to reset the rest to 0 first.
|
||||||
@@ -862,29 +849,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var unreadCategoryCount in unreadCategoryCountResult)
|
|
||||||
{
|
|
||||||
if (MenuItems.TryGetCategoryMenuItem(unreadCategoryCount.CategoryId, out var categoryMenuItem))
|
|
||||||
{
|
|
||||||
if (categoryMenuItem is IMergedMailCategoryMenuItem mergedCategoryMenuItem)
|
|
||||||
{
|
|
||||||
await ExecuteUIThread(() =>
|
|
||||||
{
|
|
||||||
categoryMenuItem.UnreadItemCount = unreadCategoryCountResult
|
|
||||||
.Where(a => mergedCategoryMenuItem.Categories.Any(b => b.Id == a.CategoryId))
|
|
||||||
.Sum(a => a.UnreadItemCount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ExecuteUIThread(() =>
|
|
||||||
{
|
|
||||||
categoryMenuItem.UnreadItemCount = unreadCategoryCount.UnreadItemCount;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update unread badge after all unread counts are updated.
|
// Update unread badge after all unread counts are updated.
|
||||||
await _notificationBuilder.UpdateTaskbarIconBadgeAsync();
|
await _notificationBuilder.UpdateTaskbarIconBadgeAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,251 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
|
||||||
using Wino.Core.Requests.Category;
|
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels;
|
|
||||||
|
|
||||||
public partial class MailCategoryManagementPageViewModel : MailBaseViewModel
|
|
||||||
{
|
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
private readonly IAccountService _accountService;
|
|
||||||
private readonly IMailDialogService _dialogService;
|
|
||||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(CanRefresh))]
|
|
||||||
public partial MailAccount Account { get; set; }
|
|
||||||
|
|
||||||
public ObservableCollection<MailCategory> Categories { get; } = [];
|
|
||||||
|
|
||||||
public bool CanRefresh => Account?.ProviderType == MailProviderType.Outlook;
|
|
||||||
public bool HasCategories => Categories.Count > 0;
|
|
||||||
|
|
||||||
public MailCategoryManagementPageViewModel(
|
|
||||||
IMailCategoryService mailCategoryService,
|
|
||||||
IAccountService accountService,
|
|
||||||
IMailDialogService dialogService,
|
|
||||||
IWinoRequestDelegator winoRequestDelegator)
|
|
||||||
{
|
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
_accountService = accountService;
|
|
||||||
_dialogService = dialogService;
|
|
||||||
_winoRequestDelegator = winoRequestDelegator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
||||||
{
|
|
||||||
base.OnNavigatedTo(mode, parameters);
|
|
||||||
|
|
||||||
if (parameters is not Guid accountId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (Account != null)
|
|
||||||
{
|
|
||||||
await LoadCategoriesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private Task AddCategoryAsync()
|
|
||||||
=> CreateOrUpdateCategoryAsync();
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task RefreshCategoriesAsync()
|
|
||||||
{
|
|
||||||
if (!CanRefresh)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var shouldContinue = await _dialogService.ShowConfirmationDialogAsync(
|
|
||||||
Translator.MailCategoryManagementPage_RefreshConfirmationMessage,
|
|
||||||
Translator.Buttons_Refresh,
|
|
||||||
Translator.Buttons_Refresh).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!shouldContinue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _mailCategoryService.DeleteCategoriesAsync(Account.Id).ConfigureAwait(false);
|
|
||||||
await SynchronizationManager.Instance.SynchronizeCategoriesAsync(Account.Id).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await LoadCategoriesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task EditCategoryAsync(MailCategory category)
|
|
||||||
=> CreateOrUpdateCategoryAsync(category);
|
|
||||||
|
|
||||||
public async Task DeleteCategoryAsync(MailCategory category)
|
|
||||||
{
|
|
||||||
if (category == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var shouldDelete = await _dialogService.ShowConfirmationDialogAsync(
|
|
||||||
string.Format(Translator.MailCategoryManagementPage_DeleteConfirmationMessage, category.Name),
|
|
||||||
Translator.MailCategoryManagementPage_DeleteConfirmationTitle,
|
|
||||||
Translator.Buttons_Delete).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!shouldDelete)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var deleteRequest = await BuildDeleteCategoryRequestAsync(category).ConfigureAwait(false);
|
|
||||||
await _mailCategoryService.DeleteCategoryAsync(category.Id).ConfigureAwait(false);
|
|
||||||
await QueueOutlookCategoryRequestsAsync(deleteRequest).ConfigureAwait(false);
|
|
||||||
await LoadCategoriesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetFavoriteAsync(MailCategory category, bool isFavorite)
|
|
||||||
{
|
|
||||||
if (category == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _mailCategoryService.ToggleFavoriteAsync(category.Id, isFavorite).ConfigureAwait(false);
|
|
||||||
await LoadCategoriesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CreateOrUpdateCategoryAsync(MailCategory existingCategory = null)
|
|
||||||
{
|
|
||||||
var dialogResult = await _dialogService.ShowEditMailCategoryDialogAsync(existingCategory).ConfigureAwait(false);
|
|
||||||
if (dialogResult == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(dialogResult.Name))
|
|
||||||
{
|
|
||||||
await _dialogService.ShowMessageAsync(
|
|
||||||
Translator.MailCategoryDialog_InvalidNameMessage,
|
|
||||||
Translator.MailCategoryDialog_InvalidNameTitle,
|
|
||||||
WinoCustomMessageDialogIcon.Warning).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalizedName = dialogResult.Name.Trim();
|
|
||||||
var categoryIdToExclude = existingCategory?.Id;
|
|
||||||
var alreadyExists = await _mailCategoryService.CategoryNameExistsAsync(Account.Id, normalizedName, categoryIdToExclude).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (alreadyExists)
|
|
||||||
{
|
|
||||||
await _dialogService.ShowMessageAsync(
|
|
||||||
Translator.MailCategoryDialog_DuplicateMessage,
|
|
||||||
Translator.MailCategoryDialog_DuplicateTitle,
|
|
||||||
WinoCustomMessageDialogIcon.Warning).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingCategory == null)
|
|
||||||
{
|
|
||||||
var newCategory = new MailCategory
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
MailAccountId = Account.Id,
|
|
||||||
Name = normalizedName,
|
|
||||||
BackgroundColorHex = dialogResult.BackgroundColorHex,
|
|
||||||
TextColorHex = dialogResult.TextColorHex,
|
|
||||||
Source = Account.ProviderType == MailProviderType.Outlook ? MailCategorySource.Outlook : MailCategorySource.Local
|
|
||||||
};
|
|
||||||
|
|
||||||
await _mailCategoryService.CreateCategoryAsync(newCategory).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (Account.ProviderType == MailProviderType.Outlook)
|
|
||||||
{
|
|
||||||
await _winoRequestDelegator.ExecuteAsync(Account.Id, [new MailCategoryCreateRequest(newCategory)]).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var previousName = existingCategory.Name;
|
|
||||||
var previousRemoteId = existingCategory.RemoteId;
|
|
||||||
|
|
||||||
existingCategory.Name = normalizedName;
|
|
||||||
existingCategory.BackgroundColorHex = dialogResult.BackgroundColorHex;
|
|
||||||
existingCategory.TextColorHex = dialogResult.TextColorHex;
|
|
||||||
|
|
||||||
await _mailCategoryService.UpdateCategoryAsync(existingCategory).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (Account.ProviderType == MailProviderType.Outlook)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(previousRemoteId))
|
|
||||||
{
|
|
||||||
await _winoRequestDelegator.ExecuteAsync(Account.Id, [new MailCategoryCreateRequest(existingCategory)]).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var affectedMessages = await BuildAffectedMessageTargetsAsync(existingCategory.Id).ConfigureAwait(false);
|
|
||||||
var updateRequest = new MailCategoryUpdateRequest(existingCategory, previousName, previousRemoteId, affectedMessages);
|
|
||||||
await _winoRequestDelegator.ExecuteAsync(Account.Id, [updateRequest]).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await LoadCategoriesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<MailCategoryDeleteRequest> BuildDeleteCategoryRequestAsync(MailCategory category)
|
|
||||||
{
|
|
||||||
if (category == null || Account?.ProviderType != MailProviderType.Outlook)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var mailCopies = await _mailCategoryService.GetMailCopiesForCategoryAsync(category.Id).ConfigureAwait(false);
|
|
||||||
var affectedMessages = new List<MailCategoryMessageUpdateTarget>();
|
|
||||||
|
|
||||||
foreach (var mailCopy in mailCopies.Where(a => !string.IsNullOrWhiteSpace(a.Id)))
|
|
||||||
{
|
|
||||||
var remainingNames = await _mailCategoryService.GetCategoryNamesForMailAsync(mailCopy.UniqueId).ConfigureAwait(false);
|
|
||||||
var categoryNames = remainingNames
|
|
||||||
.Where(a => !string.Equals(a, category.Name, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
affectedMessages.Add(new MailCategoryMessageUpdateTarget(mailCopy.Id, categoryNames));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MailCategoryDeleteRequest(category, category.RemoteId, affectedMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IReadOnlyList<MailCategoryMessageUpdateTarget>> BuildAffectedMessageTargetsAsync(Guid categoryId)
|
|
||||||
{
|
|
||||||
var mailCopies = await _mailCategoryService.GetMailCopiesForCategoryAsync(categoryId).ConfigureAwait(false);
|
|
||||||
var affectedMessages = new List<MailCategoryMessageUpdateTarget>();
|
|
||||||
|
|
||||||
foreach (var mailCopy in mailCopies.Where(a => !string.IsNullOrWhiteSpace(a.Id)))
|
|
||||||
{
|
|
||||||
var categoryNames = await _mailCategoryService.GetCategoryNamesForMailAsync(mailCopy.UniqueId).ConfigureAwait(false);
|
|
||||||
affectedMessages.Add(new MailCategoryMessageUpdateTarget(mailCopy.Id, categoryNames));
|
|
||||||
}
|
|
||||||
|
|
||||||
return affectedMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task QueueOutlookCategoryRequestsAsync(params IRequestBase[] requests)
|
|
||||||
=> Account?.ProviderType == MailProviderType.Outlook && requests.Any(a => a != null)
|
|
||||||
? _winoRequestDelegator.ExecuteAsync(Account.Id, requests.Where(a => a != null))
|
|
||||||
: Task.CompletedTask;
|
|
||||||
|
|
||||||
private async Task LoadCategoriesAsync()
|
|
||||||
{
|
|
||||||
var categories = await _mailCategoryService.GetCategoriesAsync(Account.Id).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
|
||||||
{
|
|
||||||
Categories.Clear();
|
|
||||||
|
|
||||||
foreach (var category in categories)
|
|
||||||
{
|
|
||||||
Categories.Add(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(HasCategories));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ using Wino.Core.Domain.Models.Menus;
|
|||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Reader;
|
using Wino.Core.Domain.Models.Reader;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.Requests.Mail;
|
|
||||||
using Wino.Core.Services;
|
using Wino.Core.Services;
|
||||||
using Wino.Mail.ViewModels.Collections;
|
using Wino.Mail.ViewModels.Collections;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
@@ -78,7 +77,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
private readonly INotificationBuilder _notificationBuilder;
|
private readonly INotificationBuilder _notificationBuilder;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
private readonly IContextMenuItemService _contextMenuItemService;
|
private readonly IContextMenuItemService _contextMenuItemService;
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||||
private readonly IKeyPressService _keyPressService;
|
private readonly IKeyPressService _keyPressService;
|
||||||
private readonly IWinoLogger _winoLogger;
|
private readonly IWinoLogger _winoLogger;
|
||||||
@@ -158,8 +156,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(CanSynchronize))]
|
[NotifyPropertyChangedFor(nameof(CanSynchronize))]
|
||||||
[NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))]
|
[NotifyPropertyChangedFor(nameof(IsFolderSynchronizationEnabled))]
|
||||||
[NotifyPropertyChangedFor(nameof(IsCategoryView))]
|
|
||||||
[NotifyPropertyChangedFor(nameof(IsSyncButtonVisible))]
|
|
||||||
public partial IBaseFolderMenuItem ActiveFolder { get; set; }
|
public partial IBaseFolderMenuItem ActiveFolder { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -176,7 +172,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
INotificationBuilder notificationBuilder,
|
INotificationBuilder notificationBuilder,
|
||||||
IFolderService folderService,
|
IFolderService folderService,
|
||||||
IContextMenuItemService contextMenuItemService,
|
IContextMenuItemService contextMenuItemService,
|
||||||
IMailCategoryService mailCategoryService,
|
|
||||||
IWinoRequestDelegator winoRequestDelegator,
|
IWinoRequestDelegator winoRequestDelegator,
|
||||||
IKeyPressService keyPressService,
|
IKeyPressService keyPressService,
|
||||||
IPreferencesService preferencesService,
|
IPreferencesService preferencesService,
|
||||||
@@ -190,7 +185,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
_mimeFileService = mimeFileService;
|
_mimeFileService = mimeFileService;
|
||||||
_folderService = folderService;
|
_folderService = folderService;
|
||||||
_contextMenuItemService = contextMenuItemService;
|
_contextMenuItemService = contextMenuItemService;
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
_winoRequestDelegator = winoRequestDelegator;
|
_winoRequestDelegator = winoRequestDelegator;
|
||||||
_keyPressService = keyPressService;
|
_keyPressService = keyPressService;
|
||||||
|
|
||||||
@@ -283,11 +277,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanSynchronize => !IsCategoryView && !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
|
public bool CanSynchronize => !IsAccountSynchronizerInSynchronization && IsFolderSynchronizationEnabled;
|
||||||
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
||||||
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
||||||
public bool IsCategoryView => ActiveFolder is IMailCategoryMenuItem or IMergedMailCategoryMenuItem;
|
|
||||||
public bool IsSyncButtonVisible => !IsCategoryView;
|
|
||||||
|
|
||||||
public string SelectedMessageText => IsDragInProgress
|
public string SelectedMessageText => IsDragInProgress
|
||||||
? string.Format(Translator.MailsDragging, DraggingItemsCount)
|
? string.Format(Translator.MailsDragging, DraggingItemsCount)
|
||||||
@@ -404,12 +396,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (IsCategoryView)
|
|
||||||
{
|
|
||||||
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
|
|
||||||
}
|
|
||||||
// Merged folders don't support focused feature.
|
// Merged folders don't support focused feature.
|
||||||
else if (ActiveFolder is IMergedAccountFolderMenuItem)
|
|
||||||
|
if (ActiveFolder is IMergedAccountFolderMenuItem)
|
||||||
{
|
{
|
||||||
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
|
PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
|
||||||
}
|
}
|
||||||
@@ -556,7 +545,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task EnableFolderSynchronizationAsync()
|
private async Task EnableFolderSynchronizationAsync()
|
||||||
{
|
{
|
||||||
if (ActiveFolder == null || IsCategoryView) return;
|
if (ActiveFolder == null) return;
|
||||||
|
|
||||||
foreach (var folder in ActiveFolder.HandlingFolders)
|
foreach (var folder in ActiveFolder.HandlingFolders)
|
||||||
{
|
{
|
||||||
@@ -572,9 +561,13 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
Debug.WriteLine("Loading more...");
|
Debug.WriteLine("Loading more...");
|
||||||
await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
||||||
|
|
||||||
var initializationOptions = CreateInitializationOptions(
|
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
||||||
IsInSearchMode ? SearchQuery : string.Empty,
|
SelectedFilterOption.Type,
|
||||||
MailCollection.MailCopyIdHashSet);
|
SelectedSortingOption.Type,
|
||||||
|
PreferencesService.IsThreadingEnabled,
|
||||||
|
SelectedFolderPivot.IsFocused,
|
||||||
|
IsInSearchMode ? SearchQuery : string.Empty,
|
||||||
|
MailCollection.MailCopyIdHashSet);
|
||||||
|
|
||||||
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
|
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -681,60 +674,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems)
|
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems)
|
||||||
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
|
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
|
||||||
|
|
||||||
public async Task<(IReadOnlyList<MailCategory> Categories, IReadOnlyCollection<Guid> AssignedCategoryIds)> GetAvailableCategoriesAsync(IEnumerable<MailItemViewModel> targetItems)
|
|
||||||
{
|
|
||||||
var targetList = targetItems?.Where(a => a?.MailCopy?.AssignedAccount != null).ToList() ?? [];
|
|
||||||
if (targetList.Count == 0)
|
|
||||||
return ([], []);
|
|
||||||
|
|
||||||
var accountIds = targetList.Select(a => a.MailCopy.AssignedAccount.Id).Distinct().ToList();
|
|
||||||
if (accountIds.Count != 1)
|
|
||||||
return ([], []);
|
|
||||||
|
|
||||||
var accountId = accountIds[0];
|
|
||||||
var uniqueIds = targetList.Select(a => a.MailCopy.UniqueId).Distinct().ToList();
|
|
||||||
|
|
||||||
var categories = await _mailCategoryService.GetCategoriesAsync(accountId).ConfigureAwait(false);
|
|
||||||
var assignedCategoryIds = await _mailCategoryService.GetAssignedCategoryIdsForAllAsync(uniqueIds).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return (categories, assignedCategoryIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ToggleCategoryAssignmentAsync(MailCategory category, IEnumerable<MailItemViewModel> targetItems, bool isAssignedToAll)
|
|
||||||
{
|
|
||||||
var targetList = targetItems?.Where(a => a?.MailCopy?.AssignedAccount != null).ToList() ?? [];
|
|
||||||
if (category == null || targetList.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var accountIds = targetList.Select(a => a.MailCopy.AssignedAccount.Id).Distinct().ToList();
|
|
||||||
if (accountIds.Count != 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var accountId = accountIds[0];
|
|
||||||
var uniqueIds = targetList.Select(a => a.MailCopy.UniqueId).Distinct().ToList();
|
|
||||||
|
|
||||||
if (isAssignedToAll)
|
|
||||||
{
|
|
||||||
await _mailCategoryService.UnassignCategoryAsync(category.Id, uniqueIds).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _mailCategoryService.AssignCategoryAsync(category.Id, uniqueIds).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetList.First().MailCopy.AssignedAccount.ProviderType != MailProviderType.Outlook)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var requests = new List<IRequestBase>();
|
|
||||||
foreach (var mailItem in targetList.Select(a => a.MailCopy).DistinctBy(a => a.UniqueId))
|
|
||||||
{
|
|
||||||
var categoryNames = await _mailCategoryService.GetCategoryNamesForMailAsync(mailItem.UniqueId).ConfigureAwait(false);
|
|
||||||
requests.Add(new MailCategoryAssignmentRequest(mailItem, category.Id, category.Name, categoryNames, !isAssignedToAll));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _winoRequestDelegator.ExecuteAsync(accountId, requests).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ShouldPreventItemAdd(MailCopy mailItem)
|
private bool ShouldPreventItemAdd(MailCopy mailItem)
|
||||||
{
|
{
|
||||||
bool condition = mailItem.IsRead
|
bool condition = mailItem.IsRead
|
||||||
@@ -752,7 +691,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
=> ActiveFolder?.SpecialFolderType == SpecialFolderType.Draft;
|
=> ActiveFolder?.SpecialFolderType == SpecialFolderType.Draft;
|
||||||
|
|
||||||
private bool BelongsToActiveFolder(MailCopy mailItem)
|
private bool BelongsToActiveFolder(MailCopy mailItem)
|
||||||
=> !IsCategoryView && mailItem?.AssignedFolder != null && ActiveFolder?.HandlingFolders?.Any(a => a.Id == mailItem.AssignedFolder.Id) == true;
|
=> mailItem?.AssignedFolder != null && ActiveFolder?.HandlingFolders?.Any(a => a.Id == mailItem.AssignedFolder.Id) == true;
|
||||||
|
|
||||||
private bool ShouldIncludeByThread(MailCopy mailItem)
|
private bool ShouldIncludeByThread(MailCopy mailItem)
|
||||||
=> PreferencesService.IsThreadingEnabled
|
=> PreferencesService.IsThreadingEnabled
|
||||||
@@ -1130,38 +1069,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MailListInitializationOptions CreateInitializationOptions(
|
|
||||||
string searchQuery,
|
|
||||||
System.Collections.Concurrent.ConcurrentDictionary<Guid, bool> existingUniqueIds,
|
|
||||||
List<MailCopy> preFetchedMailCopies = null,
|
|
||||||
bool deduplicateByServerId = false)
|
|
||||||
{
|
|
||||||
var options = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
|
||||||
SelectedFilterOption.Type,
|
|
||||||
SelectedSortingOption.Type,
|
|
||||||
PreferencesService.IsThreadingEnabled,
|
|
||||||
SelectedFolderPivot.IsFocused,
|
|
||||||
searchQuery,
|
|
||||||
existingUniqueIds,
|
|
||||||
preFetchedMailCopies,
|
|
||||||
DeduplicateByServerId: deduplicateByServerId);
|
|
||||||
|
|
||||||
if (!IsCategoryView)
|
|
||||||
return options;
|
|
||||||
|
|
||||||
var categoryIds = ActiveFolder switch
|
|
||||||
{
|
|
||||||
IMailCategoryMenuItem singleCategoryMenuItem => new List<Guid> { singleCategoryMenuItem.MailCategory.Id },
|
|
||||||
IMergedMailCategoryMenuItem mergedCategoryMenuItem => mergedCategoryMenuItem.Categories.Select(a => a.Id).ToList(),
|
|
||||||
_ => []
|
|
||||||
};
|
|
||||||
|
|
||||||
return options with
|
|
||||||
{
|
|
||||||
CategoryIds = categoryIds
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task PerformOnlineSearchAsync()
|
private async Task PerformOnlineSearchAsync()
|
||||||
{
|
{
|
||||||
@@ -1311,11 +1218,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var initializationOptions = CreateInitializationOptions(
|
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
||||||
isDoingOnlineSearch ? string.Empty : SearchQuery,
|
SelectedFilterOption.Type,
|
||||||
MailCollection.MailCopyIdHashSet,
|
SelectedSortingOption.Type,
|
||||||
onlineSearchItems,
|
PreferencesService.IsThreadingEnabled,
|
||||||
isDoingOnlineSearch);
|
SelectedFolderPivot.IsFocused,
|
||||||
|
isDoingOnlineSearch ? string.Empty : SearchQuery,
|
||||||
|
MailCollection.MailCopyIdHashSet,
|
||||||
|
onlineSearchItems,
|
||||||
|
DeduplicateByServerId: isDoingOnlineSearch);
|
||||||
|
|
||||||
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
|
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.Windows.AppNotifications;
|
using System;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
@@ -49,8 +49,31 @@ internal static class ToastActivationResolver
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryResolveMode(NotificationArguments toastArguments, out WinoApplicationMode mode)
|
||||||
|
{
|
||||||
|
mode = WinoApplicationMode.Mail;
|
||||||
|
|
||||||
|
if (!toastArguments.TryGetValue(Constants.ToastModeKey, out string toastMode))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.Equals(toastMode, Constants.ToastModeCalendar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mode = WinoApplicationMode.Calendar;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(toastMode, Constants.ToastModeMail, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mode = WinoApplicationMode.Mail;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ContainsKnownToastKey(NotificationArguments toastArguments)
|
private static bool ContainsKnownToastKey(NotificationArguments toastArguments)
|
||||||
=> toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) ||
|
=> toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) ||
|
||||||
|
toastArguments.TryGetValue(Constants.ToastModeKey, out string _) ||
|
||||||
toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) ||
|
toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) ||
|
||||||
toastArguments.TryGetValue(Constants.ToastActionKey, out string _);
|
toastArguments.TryGetValue(Constants.ToastActionKey, out string _);
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-45
@@ -4,8 +4,8 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
@@ -16,7 +16,6 @@ using Microsoft.Windows.AppNotifications;
|
|||||||
using MimeKit.Cryptography;
|
using MimeKit.Cryptography;
|
||||||
using Windows.ApplicationModel.Activation;
|
using Windows.ApplicationModel.Activation;
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
using Windows.ApplicationModel.DataTransfer.ShareTarget;
|
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
using Wino.Calendar.ViewModels;
|
using Wino.Calendar.ViewModels;
|
||||||
using Wino.Calendar.ViewModels.Interfaces;
|
using Wino.Calendar.ViewModels.Interfaces;
|
||||||
@@ -364,7 +363,6 @@ public partial class App : WinoApplication,
|
|||||||
services.AddTransient(typeof(StoragePageViewModel));
|
services.AddTransient(typeof(StoragePageViewModel));
|
||||||
services.AddTransient(typeof(WinoAccountManagementPageViewModel));
|
services.AddTransient(typeof(WinoAccountManagementPageViewModel));
|
||||||
services.AddTransient(typeof(AliasManagementPageViewModel));
|
services.AddTransient(typeof(AliasManagementPageViewModel));
|
||||||
services.AddTransient(typeof(MailCategoryManagementPageViewModel));
|
|
||||||
services.AddTransient(typeof(ContactsPageViewModel));
|
services.AddTransient(typeof(ContactsPageViewModel));
|
||||||
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
|
||||||
services.AddTransient(typeof(EmailTemplatesPageViewModel));
|
services.AddTransient(typeof(EmailTemplatesPageViewModel));
|
||||||
@@ -544,23 +542,20 @@ public partial class App : WinoApplication,
|
|||||||
{
|
{
|
||||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
|
|
||||||
if (activationArgs.Kind != ExtendedActivationKind.AppNotification ||
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
||||||
activationArgs.Data is not AppNotificationActivatedEventArgs toastArgs ||
|
activationArgs.Data is ILaunchActivatedEventArgs launchArgs &&
|
||||||
!TryMarkInitialNotificationActivationHandled())
|
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments) &&
|
||||||
|
TryMarkInitialNotificationActivationHandled())
|
||||||
{
|
{
|
||||||
return;
|
LogActivation($"Processing initial toast launch activation from application startup. Arguments: {launchArgs.Arguments}");
|
||||||
|
|
||||||
|
await EnsureActivationInfrastructureAsync();
|
||||||
|
await HandleToastActivationAsync(launchToastArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogActivation($"Processing initial notification activation from application startup. Arguments: {toastArgs.Argument}");
|
|
||||||
|
|
||||||
await EnsureActivationInfrastructureAsync();
|
|
||||||
await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
|
private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
// AppNotification callbacks are not guaranteed to run on the UI thread.
|
|
||||||
// Marshal toast handling to the window dispatcher before touching window APIs.
|
|
||||||
if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args.Argument, args.UserInput)) == true)
|
if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args.Argument, args.UserInput)) == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -570,19 +565,7 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
private void TryRegisterAppNotifications()
|
private void TryRegisterAppNotifications()
|
||||||
{
|
{
|
||||||
var notificationManager = AppNotificationManager.Default;
|
// Classic targeted toasts use normal launch activation instead of COM toast activators.
|
||||||
|
|
||||||
notificationManager.NotificationInvoked -= AppNotificationInvoked;
|
|
||||||
notificationManager.NotificationInvoked += AppNotificationInvoked;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
notificationManager.Register();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LogActivation($"App notification registration failed: {ex.GetType().Name} - {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -612,7 +595,7 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
if (calendarAction == Constants.ToastCalendarSnoozeAction)
|
if (calendarAction == Constants.ToastCalendarSnoozeAction)
|
||||||
{
|
{
|
||||||
await HandleCalendarToastSnoozeAsync(userInput, calendarItemId);
|
await HandleCalendarToastSnoozeAsync(toastArguments, userInput, calendarItemId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,6 +640,26 @@ public partial class App : WinoApplication,
|
|||||||
return HandleToastActivationAsync(toastArguments, userInput);
|
return HandleToastActivationAsync(toastArguments, userInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int? GetToastSnoozeDurationMinutes(NotificationArguments toastArguments, IDictionary<string, string>? userInput)
|
||||||
|
{
|
||||||
|
if (toastArguments.TryGetValue(Constants.ToastCalendarSnoozeDurationMinutesKey, out var snoozeDurationValue) &&
|
||||||
|
int.TryParse(snoozeDurationValue, out var snoozeDurationMinutes) &&
|
||||||
|
snoozeDurationMinutes > 0)
|
||||||
|
{
|
||||||
|
return snoozeDurationMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInput != null &&
|
||||||
|
userInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) &&
|
||||||
|
int.TryParse(selectedValue, out snoozeDurationMinutes) &&
|
||||||
|
snoozeDurationMinutes > 0)
|
||||||
|
{
|
||||||
|
return snoozeDurationMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> HandleShareTargetActivationAsync(AppActivationArguments activationArgs, bool activateWindow)
|
private async Task<bool> HandleShareTargetActivationAsync(AppActivationArguments activationArgs, bool activateWindow)
|
||||||
{
|
{
|
||||||
if (activationArgs.Kind != ExtendedActivationKind.ShareTarget ||
|
if (activationArgs.Kind != ExtendedActivationKind.ShareTarget ||
|
||||||
@@ -811,9 +814,59 @@ public partial class App : WinoApplication,
|
|||||||
navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleCalendarToastSnoozeAsync(IDictionary<string, string>? userInput, Guid calendarItemId)
|
private async Task<object?> TryCreateToastNavigationParameterAsync(NotificationArguments toastArguments)
|
||||||
{
|
{
|
||||||
if (!TryGetSnoozeDurationMinutes(userInput, out var snoozeDurationMinutes))
|
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
||||||
|
calendarAction == Constants.ToastCalendarNavigateAction &&
|
||||||
|
toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) &&
|
||||||
|
Guid.TryParse(calendarItemIdString, out Guid calendarItemId))
|
||||||
|
{
|
||||||
|
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||||
|
var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId);
|
||||||
|
|
||||||
|
if (calendarItem != null)
|
||||||
|
{
|
||||||
|
return new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(WinoApplicationMode Mode, object? Parameter)?> TryResolveToastActivationTargetAsync(AppActivationArguments activationArgs)
|
||||||
|
{
|
||||||
|
NotificationArguments? toastArguments = null;
|
||||||
|
|
||||||
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
||||||
|
activationArgs.Data is ILaunchActivatedEventArgs launchArgs &&
|
||||||
|
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
||||||
|
{
|
||||||
|
toastArguments = launchToastArguments;
|
||||||
|
}
|
||||||
|
else if (activationArgs.Kind == ExtendedActivationKind.AppNotification &&
|
||||||
|
activationArgs.Data is AppNotificationActivatedEventArgs appNotificationArgs &&
|
||||||
|
ToastActivationResolver.TryParse(appNotificationArgs.Argument, out var appNotificationToastArguments))
|
||||||
|
{
|
||||||
|
toastArguments = appNotificationToastArguments;
|
||||||
|
}
|
||||||
|
else if (activationArgs.Data is ToastNotificationActivatedEventArgs classicToastArgs &&
|
||||||
|
ToastActivationResolver.TryParse(classicToastArgs.Argument, out var classicToastArguments))
|
||||||
|
{
|
||||||
|
toastArguments = classicToastArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastArguments == null ||
|
||||||
|
!ToastActivationResolver.TryResolveMode(toastArguments, out var mode))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (mode, await TryCreateToastNavigationParameterAsync(toastArguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCalendarToastSnoozeAsync(NotificationArguments toastArguments, IDictionary<string, string>? userInput, Guid calendarItemId)
|
||||||
|
{
|
||||||
|
if (!TryGetSnoozeDurationMinutes(toastArguments, userInput, out var snoozeDurationMinutes))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||||
@@ -837,20 +890,13 @@ public partial class App : WinoApplication,
|
|||||||
await nativeAppService.LaunchUriAsync(joinUri);
|
await nativeAppService.LaunchUriAsync(joinUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetSnoozeDurationMinutes(IDictionary<string, string>? userInput, out int snoozeDurationMinutes)
|
private bool TryGetSnoozeDurationMinutes(NotificationArguments toastArguments, IDictionary<string, string>? userInput, out int snoozeDurationMinutes)
|
||||||
{
|
{
|
||||||
snoozeDurationMinutes = _preferencesService?.DefaultSnoozeDurationInMinutes ?? 0;
|
snoozeDurationMinutes = GetToastSnoozeDurationMinutes(toastArguments, userInput)
|
||||||
|
?? _preferencesService?.DefaultSnoozeDurationInMinutes
|
||||||
|
?? 0;
|
||||||
|
|
||||||
if (userInput == null ||
|
return snoozeDurationMinutes > 0;
|
||||||
!userInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) ||
|
|
||||||
selectedValue == null)
|
|
||||||
{
|
|
||||||
return snoozeDurationMinutes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedText = selectedValue.ToString();
|
|
||||||
|
|
||||||
return int.TryParse(selectedText, out snoozeDurationMinutes) && snoozeDurationMinutes > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1322,7 +1368,6 @@ public partial class App : WinoApplication,
|
|||||||
return synchronizationType switch
|
return synchronizationType switch
|
||||||
{
|
{
|
||||||
MailSynchronizationType.Alias => Translator.Exception_FailedToSynchronizeAliases,
|
MailSynchronizationType.Alias => Translator.Exception_FailedToSynchronizeAliases,
|
||||||
MailSynchronizationType.Categories => Translator.Exception_FailedToSynchronizeCategories,
|
|
||||||
MailSynchronizationType.UpdateProfile => Translator.Exception_FailedToSynchronizeProfileInformation,
|
MailSynchronizationType.UpdateProfile => Translator.Exception_FailedToSynchronizeProfileInformation,
|
||||||
_ => Translator.Exception_FailedToSynchronizeFolders
|
_ => Translator.Exception_FailedToSynchronizeFolders
|
||||||
};
|
};
|
||||||
@@ -1586,7 +1631,22 @@ public partial class App : WinoApplication,
|
|||||||
}
|
}
|
||||||
else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode))
|
else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode))
|
||||||
{
|
{
|
||||||
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(redirectedMode));
|
var navigationService = Services.GetRequiredService<INavigationService>();
|
||||||
|
var toastActivationTarget = await TryResolveToastActivationTargetAsync(args);
|
||||||
|
|
||||||
|
if (toastActivationTarget is { Parameter: CalendarItemTarget calendarTarget })
|
||||||
|
{
|
||||||
|
navigationService.ChangeApplicationMode(toastActivationTarget.Value.Mode, new ShellModeActivationContext
|
||||||
|
{
|
||||||
|
SuppressStartupFlows = true,
|
||||||
|
Parameter = calendarTarget
|
||||||
|
});
|
||||||
|
navigationService.Navigate(WinoPage.EventDetailsPage, calendarTarget);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(redirectedMode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1599,7 +1659,6 @@ public partial class App : WinoApplication,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch to UI thread since this is called from Program.OnActivated.
|
|
||||||
if (MainWindow?.DispatcherQueue.TryEnqueue(() => _ = HandleRedirectedActivationAsync()) == true)
|
if (MainWindow?.DispatcherQueue.TryEnqueue(() => _ = HandleRedirectedActivationAsync()) == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1644,6 +1703,13 @@ public partial class App : WinoApplication,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activationArgs.Data is ToastNotificationActivatedEventArgs classicToastArgs &&
|
||||||
|
ToastActivationResolver.TryParse(classicToastArgs.Argument, out var classicToastArguments) &&
|
||||||
|
ToastActivationResolver.TryResolveMode(classicToastArguments, out mode))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (activationArgs.Kind == ExtendedActivationKind.File &&
|
if (activationArgs.Kind == ExtendedActivationKind.File &&
|
||||||
activationArgs.Data is IFileActivatedEventArgs fileArgs)
|
activationArgs.Data is IFileActivatedEventArgs fileArgs)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -296,18 +296,12 @@ public sealed partial class WebViewEditorControl : Control, IDisposable, IEditor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusControlAsWell)
|
|
||||||
{
|
|
||||||
Focus(FocusState.Programmatic);
|
|
||||||
_chromium.Focus(FocusState.Programmatic);
|
|
||||||
_chromium.Focus(FocusState.Keyboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _chromium.ExecuteScriptSafeAsync("focusEditor();");
|
await _chromium.ExecuteScriptSafeAsync("focusEditor();");
|
||||||
|
|
||||||
if (focusControlAsWell)
|
if (focusControlAsWell)
|
||||||
{
|
{
|
||||||
_chromium.Focus(FocusState.Keyboard);
|
_chromium.Focus(FocusState.Keyboard);
|
||||||
|
_chromium.Focus(FocusState.Programmatic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<ContentDialog
|
|
||||||
x:Class="Wino.Dialogs.EditMailCategoryDialog"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
|
||||||
xmlns:mailModels="using:Wino.Core.Domain.Models.MailItem"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
DefaultButton="Primary"
|
|
||||||
PrimaryButtonClick="SaveClicked"
|
|
||||||
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Save, Mode=OneTime}"
|
|
||||||
SecondaryButtonClick="CancelClicked"
|
|
||||||
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel, Mode=OneTime}"
|
|
||||||
Style="{StaticResource WinoDialogStyle}"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
|
|
||||||
<ContentDialog.Resources>
|
|
||||||
<x:Double x:Key="ContentDialogMinWidth">520</x:Double>
|
|
||||||
<x:Double x:Key="ContentDialogMaxWidth">520</x:Double>
|
|
||||||
</ContentDialog.Resources>
|
|
||||||
|
|
||||||
<StackPanel Spacing="16">
|
|
||||||
<TextBox
|
|
||||||
x:Name="CategoryNameTextBox"
|
|
||||||
Header="{x:Bind domain:Translator.MailCategoryDialog_Name, Mode=OneTime}"
|
|
||||||
PlaceholderText="{x:Bind domain:Translator.MailCategoryDialog_NamePlaceholder, Mode=OneTime}"
|
|
||||||
Text="{x:Bind CategoryName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
TextChanged="CategoryNameTextChanged" />
|
|
||||||
|
|
||||||
<StackPanel Spacing="8">
|
|
||||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.MailCategoryDialog_Color, Mode=OneTime}" />
|
|
||||||
|
|
||||||
<GridView
|
|
||||||
x:Name="ColorsGridView"
|
|
||||||
IsItemClickEnabled="True"
|
|
||||||
ItemClick="ColorOptionClicked"
|
|
||||||
ItemsSource="{x:Bind AvailableColors, Mode=OneWay}"
|
|
||||||
SelectedItem="{x:Bind SelectedColor, Mode=TwoWay}"
|
|
||||||
SelectionMode="Single">
|
|
||||||
<GridView.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="mailModels:MailCategoryColorOption">
|
|
||||||
<Border
|
|
||||||
Width="100"
|
|
||||||
Padding="8,6"
|
|
||||||
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}"
|
|
||||||
BorderBrush="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="10">
|
|
||||||
<TextBlock
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
Text="Preview" />
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</GridView.ItemTemplate>
|
|
||||||
</GridView>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ContentDialog>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
|
||||||
|
|
||||||
namespace Wino.Dialogs;
|
|
||||||
|
|
||||||
public sealed partial class EditMailCategoryDialog : ContentDialog
|
|
||||||
{
|
|
||||||
public MailCategoryDialogResult? Result { get; private set; }
|
|
||||||
public string CategoryName { get; set; }
|
|
||||||
public MailCategoryColorOption? SelectedColor { get; set; }
|
|
||||||
|
|
||||||
public System.Collections.Generic.IReadOnlyList<MailCategoryColorOption> AvailableColors => MailCategoryPalette.DefaultOptions;
|
|
||||||
|
|
||||||
public EditMailCategoryDialog(MailCategory? category = null)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
Title = category == null ? Translator.MailCategoryDialog_CreateTitle : Translator.MailCategoryDialog_EditTitle;
|
|
||||||
CategoryName = category?.Name ?? string.Empty;
|
|
||||||
SelectedColor = MailCategoryPalette.DefaultOptions.FirstOrDefault(a =>
|
|
||||||
a.BackgroundColorHex == category?.BackgroundColorHex &&
|
|
||||||
a.TextColorHex == category?.TextColorHex) ?? MailCategoryPalette.DefaultOptions.First();
|
|
||||||
|
|
||||||
IsPrimaryButtonEnabled = !string.IsNullOrWhiteSpace(CategoryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CategoryNameTextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
=> IsPrimaryButtonEnabled = !string.IsNullOrWhiteSpace(CategoryNameTextBox.Text) && SelectedColor != null;
|
|
||||||
|
|
||||||
private void ColorOptionClicked(object sender, ItemClickEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.ClickedItem is MailCategoryColorOption option)
|
|
||||||
{
|
|
||||||
SelectedColor = option;
|
|
||||||
ColorsGridView.SelectedItem = option;
|
|
||||||
IsPrimaryButtonEnabled = !string.IsNullOrWhiteSpace(CategoryNameTextBox.Text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
|
||||||
{
|
|
||||||
if (SelectedColor == null)
|
|
||||||
{
|
|
||||||
args.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result = new MailCategoryDialogResult(CategoryNameTextBox.Text?.Trim(), SelectedColor.BackgroundColorHex, SelectedColor.TextColorHex);
|
|
||||||
Hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
|
||||||
{
|
|
||||||
Hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,8 @@
|
|||||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
|
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
|
||||||
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
||||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
|
||||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
|
||||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
IgnorableNamespaces="uap uap10 rescap com desktop">
|
IgnorableNamespaces="uap uap10 rescap">
|
||||||
|
|
||||||
<!-- Publisher Cache Folders -->
|
<!-- Publisher Cache Folders -->
|
||||||
<Extensions>
|
<Extensions>
|
||||||
@@ -23,7 +21,7 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="58272BurakKSE.WinoMailPreview"
|
Name="58272BurakKSE.WinoMailPreview"
|
||||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||||
Version="2.0.2.0" />
|
Version="2.0.1.0" />
|
||||||
|
|
||||||
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||||
|
|
||||||
@@ -47,16 +45,15 @@
|
|||||||
Executable="$targetnametoken$.exe"
|
Executable="$targetnametoken$.exe"
|
||||||
EntryPoint="$targetentrypoint$"
|
EntryPoint="$targetentrypoint$"
|
||||||
uap10:Parameters="--wino-mail">
|
uap10:Parameters="--wino-mail">
|
||||||
<uap:VisualElements
|
<uap:VisualElements
|
||||||
DisplayName="Wino Mail"
|
DisplayName="Wino Mail"
|
||||||
Description="Wino.Mail.WinUI"
|
Description="Wino Mail"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="transparent"
|
||||||
Square150x150Logo="Assets\AppEntries\MailAssets\Square150x150Logo.png"
|
Square150x150Logo="Assets\AppEntries\MailAssets\Square150x150Logo.png"
|
||||||
Square44x44Logo="Assets\AppEntries\MailAssets\Square44x44Logo.png">
|
Square44x44Logo="Assets\AppEntries\MailAssets\Square44x44Logo.png">
|
||||||
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\MailAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\MailAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\MailAssets\LargeTile.png"/>
|
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\MailAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\MailAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\MailAssets\LargeTile.png"/>
|
||||||
<uap:SplashScreen Image="Assets\AppEntries\MailAssets\SplashScreen.png" />
|
<uap:SplashScreen Image="Assets\AppEntries\MailAssets\SplashScreen.png" />
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
|
|
||||||
<Extensions>
|
<Extensions>
|
||||||
<uap5:Extension Category="windows.startupTask">
|
<uap5:Extension Category="windows.startupTask">
|
||||||
<uap5:StartupTask
|
<uap5:StartupTask
|
||||||
@@ -65,19 +62,6 @@
|
|||||||
DisplayName="Wino Startup Service" />
|
DisplayName="Wino Startup Service" />
|
||||||
</uap5:Extension>
|
</uap5:Extension>
|
||||||
|
|
||||||
<!-- App notification activation -->
|
|
||||||
<desktop:Extension Category="windows.toastNotificationActivation">
|
|
||||||
<desktop:ToastNotificationActivation ToastActivatorCLSID="72c6d2d0-2538-44fe-a1b1-499f47bb1181" />
|
|
||||||
</desktop:Extension>
|
|
||||||
|
|
||||||
<com:Extension Category="windows.comServer">
|
|
||||||
<com:ComServer>
|
|
||||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Toast activator">
|
|
||||||
<com:Class Id="72c6d2d0-2538-44fe-a1b1-499f47bb1181" DisplayName="Toast activator"/>
|
|
||||||
</com:ExeServer>
|
|
||||||
</com:ComServer>
|
|
||||||
</com:Extension>
|
|
||||||
|
|
||||||
<!-- Protocol activation: mailto -->
|
<!-- Protocol activation: mailto -->
|
||||||
<uap:Extension Category="windows.protocol">
|
<uap:Extension Category="windows.protocol">
|
||||||
<uap:Protocol Name="mailto" />
|
<uap:Protocol Name="mailto" />
|
||||||
@@ -118,55 +102,43 @@
|
|||||||
</Extensions>
|
</Extensions>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
||||||
<Application Id="CalendarApp"
|
<Application Id="CalendarApp"
|
||||||
Executable="$targetnametoken$.exe"
|
Executable="$targetnametoken$.exe"
|
||||||
EntryPoint="$targetentrypoint$"
|
EntryPoint="$targetentrypoint$"
|
||||||
uap10:Parameters="--wino-calendar">
|
uap10:Parameters="--wino-calendar">
|
||||||
<uap:VisualElements
|
<uap:VisualElements
|
||||||
DisplayName="Wino Calendar"
|
DisplayName="Wino Calendar"
|
||||||
Description="Wino.Mail.WinUI.Calendar"
|
Description="Wino Calendar"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="transparent"
|
||||||
Square150x150Logo="Assets\AppEntries\CalendarAssets\Square150x150Logo.png"
|
Square150x150Logo="Assets\AppEntries\CalendarAssets\Square150x150Logo.png"
|
||||||
Square44x44Logo="Assets\AppEntries\CalendarAssets\Square44x44Logo.png">
|
Square44x44Logo="Assets\AppEntries\CalendarAssets\Square44x44Logo.png">
|
||||||
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\CalendarAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\CalendarAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\CalendarAssets\LargeTile.png"/>
|
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\CalendarAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\CalendarAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\CalendarAssets\LargeTile.png"/>
|
||||||
<uap:SplashScreen Image="Assets\AppEntries\CalendarAssets\SplashScreen.png" />
|
<uap:SplashScreen Image="Assets\AppEntries\CalendarAssets\SplashScreen.png" />
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
|
|
||||||
<Extensions>
|
<Extensions>
|
||||||
<desktop:Extension Category="windows.toastNotificationActivation">
|
<uap:Extension Category="windows.protocol">
|
||||||
<desktop:ToastNotificationActivation ToastActivatorCLSID="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" />
|
<uap:Protocol Name="webcal">
|
||||||
</desktop:Extension>
|
<uap:DisplayName>Calendar Protocol</uap:DisplayName>
|
||||||
|
</uap:Protocol>
|
||||||
|
</uap:Extension>
|
||||||
|
|
||||||
<com:Extension Category="windows.comServer">
|
<uap:Extension Category="windows.protocol">
|
||||||
<com:ComServer>
|
<uap:Protocol Name="webcals">
|
||||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Calendar toast activator">
|
<uap:DisplayName>Calendar Protocol (Secure)</uap:DisplayName>
|
||||||
<com:Class Id="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" DisplayName="Calendar toast activator"/>
|
</uap:Protocol>
|
||||||
</com:ExeServer>
|
</uap:Extension>
|
||||||
</com:ComServer>
|
|
||||||
</com:Extension>
|
|
||||||
|
|
||||||
<uap:Extension Category="windows.protocol">
|
<uap:Extension Category="windows.fileTypeAssociation">
|
||||||
<uap:Protocol Name="webcal">
|
<uap:FileTypeAssociation Name="ics">
|
||||||
<uap:DisplayName>Calendar Protocol</uap:DisplayName>
|
<uap:Logo>Assets\AppEntries\CalendarAssets\Square44x44Logo.png</uap:Logo>
|
||||||
</uap:Protocol>
|
<uap:SupportedFileTypes>
|
||||||
</uap:Extension>
|
<uap:FileType>.ics</uap:FileType>
|
||||||
|
</uap:SupportedFileTypes>
|
||||||
<uap:Extension Category="windows.protocol">
|
</uap:FileTypeAssociation>
|
||||||
<uap:Protocol Name="webcals">
|
</uap:Extension>
|
||||||
<uap:DisplayName>Calendar Protocol (Secure)</uap:DisplayName>
|
</Extensions>
|
||||||
</uap:Protocol>
|
</Application>
|
||||||
</uap:Extension>
|
|
||||||
|
|
||||||
<uap:Extension Category="windows.fileTypeAssociation">
|
|
||||||
<uap:FileTypeAssociation Name="ics">
|
|
||||||
<uap:Logo>Assets\AppEntries\CalendarAssets\Square44x44Logo.png</uap:Logo>
|
|
||||||
<uap:SupportedFileTypes>
|
|
||||||
<uap:FileType>.ics</uap:FileType>
|
|
||||||
</uap:SupportedFileTypes>
|
|
||||||
</uap:FileTypeAssociation>
|
|
||||||
</uap:Extension>
|
|
||||||
</Extensions>
|
|
||||||
</Application>
|
|
||||||
</Applications>
|
</Applications>
|
||||||
|
|
||||||
<Capabilities>
|
<Capabilities>
|
||||||
|
|||||||
@@ -207,6 +207,13 @@ public class Program
|
|||||||
: true;
|
: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.Data is Windows.ApplicationModel.Activation.ToastNotificationActivatedEventArgs classicToastArgs)
|
||||||
|
{
|
||||||
|
return ToastActivationResolver.TryParse(classicToastArgs.Argument, out var toastArguments)
|
||||||
|
? ToastActivationResolver.ShouldBringToForeground(toastArguments)
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Kind == ExtendedActivationKind.Launch &&
|
if (args.Kind == ExtendedActivationKind.Launch &&
|
||||||
args.Data is Windows.ApplicationModel.Activation.ILaunchActivatedEventArgs launchArgs &&
|
args.Data is Windows.ApplicationModel.Activation.ILaunchActivatedEventArgs launchArgs &&
|
||||||
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector
|
|||||||
public DataTemplate NewMailTemplate { get; set; } = null!;
|
public DataTemplate NewMailTemplate { get; set; } = null!;
|
||||||
public DataTemplate CalendarNewEventTemplate { get; set; } = null!;
|
public DataTemplate CalendarNewEventTemplate { get; set; } = null!;
|
||||||
public DataTemplate CategoryItemsTemplate { get; set; } = null!;
|
public DataTemplate CategoryItemsTemplate { get; set; } = null!;
|
||||||
public DataTemplate MergedCategoryItemsTemplate { get; set; } = null!;
|
|
||||||
public DataTemplate FixAuthenticationIssueTemplate { get; set; } = null!;
|
public DataTemplate FixAuthenticationIssueTemplate { get; set; } = null!;
|
||||||
public DataTemplate FixMissingFolderConfigTemplate { get; set; } = null!;
|
public DataTemplate FixMissingFolderConfigTemplate { get; set; } = null!;
|
||||||
|
|
||||||
@@ -59,10 +58,6 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector
|
|||||||
return MergedAccountTemplate;
|
return MergedAccountTemplate;
|
||||||
else if (item is MergedAccountMoreFolderMenuItem)
|
else if (item is MergedAccountMoreFolderMenuItem)
|
||||||
return MergedAccountMoreExpansionItemTemplate;
|
return MergedAccountMoreExpansionItemTemplate;
|
||||||
else if (item is MailCategoryMenuItem)
|
|
||||||
return CategoryItemsTemplate;
|
|
||||||
else if (item is MergedMailCategoryMenuItem)
|
|
||||||
return MergedCategoryItemsTemplate;
|
|
||||||
else if (item is MergedAccountFolderMenuItem)
|
else if (item is MergedAccountFolderMenuItem)
|
||||||
return MergedAccountFolderTemplate;
|
return MergedAccountFolderTemplate;
|
||||||
else if (item is FolderMenuItem)
|
else if (item is FolderMenuItem)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
@@ -14,12 +15,11 @@ using Wino.Core.Domain.Models;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Dialogs;
|
|
||||||
using Wino.Mail.Dialogs;
|
|
||||||
using Wino.Mail.WinUI.Extensions;
|
using Wino.Mail.WinUI.Extensions;
|
||||||
using Wino.Mail.WinUI.Services;
|
using Wino.Mail.WinUI.Services;
|
||||||
|
using Wino.Dialogs;
|
||||||
|
using Wino.Mail.Dialogs;
|
||||||
using Wino.Messaging.Server;
|
using Wino.Messaging.Server;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
@@ -40,12 +40,6 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
_winoAccountDataSyncService = winoAccountDataSyncService;
|
_winoAccountDataSyncService = winoAccountDataSyncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowReadOnlyCalendarMessage()
|
|
||||||
=> InfoBarMessage(
|
|
||||||
Translator.CalendarReadOnly_Title,
|
|
||||||
Translator.CalendarReadOnly_Message,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
|
|
||||||
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
||||||
{
|
{
|
||||||
var createAccountAliasDialog = new CreateAccountAliasDialog()
|
var createAccountAliasDialog = new CreateAccountAliasDialog()
|
||||||
@@ -58,19 +52,6 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
return createAccountAliasDialog;
|
return createAccountAliasDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CS8625
|
|
||||||
public async Task<MailCategoryDialogResult> ShowEditMailCategoryDialogAsync(MailCategory category = null)
|
|
||||||
#pragma warning restore CS8625
|
|
||||||
{
|
|
||||||
var dialog = new EditMailCategoryDialog(category)
|
|
||||||
{
|
|
||||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
|
||||||
};
|
|
||||||
|
|
||||||
await HandleDialogPresentationAsync(dialog);
|
|
||||||
return dialog.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService)
|
public async Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.ReadComposePanePage,
|
WinoPage.ReadComposePanePage,
|
||||||
WinoPage.AppPreferencesPage,
|
WinoPage.AppPreferencesPage,
|
||||||
WinoPage.AliasManagementPage,
|
WinoPage.AliasManagementPage,
|
||||||
WinoPage.MailCategoryManagementPage,
|
|
||||||
WinoPage.ImapCalDavSettingsPage,
|
WinoPage.ImapCalDavSettingsPage,
|
||||||
WinoPage.KeyboardShortcutsPage,
|
WinoPage.KeyboardShortcutsPage,
|
||||||
WinoPage.SignatureAndEncryptionPage,
|
WinoPage.SignatureAndEncryptionPage,
|
||||||
@@ -151,7 +150,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
|
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
|
||||||
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
|
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
|
||||||
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
|
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
|
||||||
WinoPage.MailCategoryManagementPage => typeof(MailCategoryManagementPage),
|
|
||||||
WinoPage.ImapCalDavSettingsPage => typeof(ImapCalDavSettingsPage),
|
WinoPage.ImapCalDavSettingsPage => typeof(ImapCalDavSettingsPage),
|
||||||
WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage),
|
WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage),
|
||||||
WinoPage.ContactsPage => typeof(ContactsPage),
|
WinoPage.ContactsPage => typeof(ContactsPage),
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Windows.AppNotifications;
|
using CommunityToolkit.WinUI.Notifications;
|
||||||
using Microsoft.Windows.AppNotifications.Builder;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Windows.Data.Xml.Dom;
|
using Windows.Data.Xml.Dom;
|
||||||
using Windows.UI.Notifications;
|
using Windows.UI.Notifications;
|
||||||
@@ -76,9 +75,9 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
|
builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
|
||||||
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
|
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
|
||||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
|
builder.AddAudio(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||||
|
|
||||||
ShowNotification(builder);
|
ShowNotification(builder, WinoApplicationMode.Mail);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -146,9 +145,9 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AppNotificationManager.Default.RemoveByTagAsync(mailUniqueId.ToString()).AsTask().GetAwaiter().GetResult();
|
ToastNotificationManager.History.Remove(mailUniqueId.ToString());
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -164,11 +163,12 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
builder.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name));
|
builder.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name));
|
||||||
builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString());
|
builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString());
|
||||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
builder.AddButton(new AppNotificationButton(Translator.Buttons_FixAccount)
|
builder.AddButton(new ToastButton()
|
||||||
|
.SetContent(Translator.Buttons_FixAccount)
|
||||||
.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString())
|
.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail));
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail));
|
||||||
|
|
||||||
ShowNotification(builder);
|
ShowNotification(builder, WinoApplicationMode.Mail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateWebView2RuntimeMissingNotification()
|
public void CreateWebView2RuntimeMissingNotification()
|
||||||
@@ -178,7 +178,7 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message);
|
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message);
|
||||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
|
|
||||||
ShowNotification(builder);
|
ShowNotification(builder, WinoApplicationMode.Mail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateStoreUpdateNotification()
|
public void CreateStoreUpdateNotification()
|
||||||
@@ -189,7 +189,7 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall);
|
builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall);
|
||||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
|
|
||||||
ShowNotification(builder, "store-update-available");
|
ShowNotification(builder, WinoApplicationMode.Mail, "store-update-available");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
||||||
@@ -197,7 +197,7 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
if (calendarItem == null)
|
if (calendarItem == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
var builder = CreateBuilder(AppNotificationScenario.Reminder);
|
var builder = CreateBuilder(ToastScenario.Reminder);
|
||||||
var localStart = calendarItem.GetLocalStartDate();
|
var localStart = calendarItem.GetLocalStartDate();
|
||||||
var reminderContext = GetCalendarReminderContext(localStart, DateTime.Now);
|
var reminderContext = GetCalendarReminderContext(localStart, DateTime.Now);
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction);
|
builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction);
|
||||||
builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString());
|
builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString());
|
||||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar);
|
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar);
|
||||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Reminder"));
|
builder.AddAudio(new Uri("ms-winsoundevent:Notification.Reminder"));
|
||||||
|
|
||||||
var allowedSnoozeMinutes = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes(
|
var allowedSnoozeMinutes = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes(
|
||||||
reminderDurationInSeconds,
|
reminderDurationInSeconds,
|
||||||
@@ -223,40 +223,33 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
? preferredSnoozeMinutes
|
? preferredSnoozeMinutes
|
||||||
: allowedSnoozeMinutes[0];
|
: allowedSnoozeMinutes[0];
|
||||||
|
|
||||||
var selectionBox = new AppNotificationComboBox(Constants.ToastCalendarSnoozeDurationInputId)
|
builder.AddButton(new ToastButton()
|
||||||
.SetSelectedItem(defaultSnoozeMinutes.ToString());
|
.SetContent(Translator.CalendarReminder_SnoozeAction)
|
||||||
|
.SetImageUri(GetNotificationIconUri("calendar-snooze"))
|
||||||
foreach (var snoozeMinutes in allowedSnoozeMinutes)
|
|
||||||
{
|
|
||||||
selectionBox.AddItem(
|
|
||||||
snoozeMinutes.ToString(),
|
|
||||||
string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AddComboBox(selectionBox);
|
|
||||||
builder.AddButton(new AppNotificationButton(Translator.CalendarReminder_SnoozeAction)
|
|
||||||
.SetIcon(GetNotificationIconUri("calendar-snooze"))
|
|
||||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarSnoozeAction)
|
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarSnoozeAction)
|
||||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||||
|
.AddArgument(Constants.ToastCalendarSnoozeDurationMinutesKey, defaultSnoozeMinutes.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.AddButton(new AppNotificationButton(Translator.Buttons_Open)
|
builder.AddButton(new ToastButton()
|
||||||
|
.SetContent(Translator.Buttons_Open)
|
||||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction)
|
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction)
|
||||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||||
|
|
||||||
if (Uri.TryCreate(calendarItem.HtmlLink, UriKind.Absolute, out _))
|
if (Uri.TryCreate(calendarItem.HtmlLink, UriKind.Absolute, out _))
|
||||||
{
|
{
|
||||||
builder.AddButton(new AppNotificationButton(Translator.CalendarEventDetails_JoinOnline)
|
builder.AddButton(new ToastButton()
|
||||||
.SetIcon(GetNotificationIconUri("calendar-join"))
|
.SetContent(Translator.CalendarEventDetails_JoinOnline)
|
||||||
|
.SetImageUri(GetNotificationIconUri("calendar-join"))
|
||||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarJoinOnlineAction)
|
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarJoinOnlineAction)
|
||||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||||
}
|
}
|
||||||
|
|
||||||
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
|
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
|
||||||
ShowNotification(builder, tag);
|
ShowNotification(builder, WinoApplicationMode.Calendar, tag);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -278,10 +271,10 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
await stream.WriteAsync(bytes);
|
await stream.WriteAsync(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.SetAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), AppNotificationImageCrop.Default);
|
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), ToastGenericAppLogoCrop.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.SetTimeStamp(mailItem.CreationDate.ToLocalTime());
|
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||||
builder.AddText(mailItem.FromName);
|
builder.AddText(mailItem.FromName);
|
||||||
builder.AddText(mailItem.Subject);
|
builder.AddText(mailItem.Subject);
|
||||||
builder.AddText(mailItem.PreviewText);
|
builder.AddText(mailItem.PreviewText);
|
||||||
@@ -291,9 +284,9 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
||||||
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
||||||
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
||||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
|
builder.AddAudio(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||||
|
|
||||||
ShowNotification(builder, mailItem.UniqueId.ToString());
|
ShowNotification(builder, WinoApplicationMode.Mail, mailItem.UniqueId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateBadge(string applicationId, int? badgeCount)
|
private void UpdateBadge(string applicationId, int? badgeCount)
|
||||||
@@ -347,40 +340,43 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo);
|
return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppNotificationButton GetArchiveButton(Guid mailUniqueId)
|
private ToastButton GetArchiveButton(Guid mailUniqueId)
|
||||||
=> new AppNotificationButton(Translator.MailOperation_Archive)
|
=> new ToastButton()
|
||||||
.SetIcon(GetNotificationIconUri("mail-archive"))
|
.SetContent(Translator.MailOperation_Archive)
|
||||||
|
.SetImageUri(GetNotificationIconUri("mail-archive"))
|
||||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||||
.AddArgument(Constants.ToastActionKey, MailOperation.Archive.ToString())
|
.AddArgument(Constants.ToastActionKey, MailOperation.Archive.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
|
|
||||||
private AppNotificationButton GetDeleteButton(Guid mailUniqueId)
|
private ToastButton GetDeleteButton(Guid mailUniqueId)
|
||||||
=> new AppNotificationButton(Translator.MailOperation_Delete)
|
=> new ToastButton()
|
||||||
.SetIcon(GetNotificationIconUri("mail-delete"))
|
.SetContent(Translator.MailOperation_Delete)
|
||||||
|
.SetImageUri(GetNotificationIconUri("mail-delete"))
|
||||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||||
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete.ToString())
|
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
|
|
||||||
private AppNotificationButton GetMarkAsReadButton(Guid mailUniqueId)
|
private ToastButton GetMarkAsReadButton(Guid mailUniqueId)
|
||||||
=> new AppNotificationButton(Translator.MailOperation_MarkAsRead)
|
=> new ToastButton()
|
||||||
.SetIcon(GetNotificationIconUri("mail-markread"))
|
.SetContent(Translator.MailOperation_MarkAsRead)
|
||||||
|
.SetImageUri(GetNotificationIconUri("mail-markread"))
|
||||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||||
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead.ToString())
|
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead.ToString())
|
||||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||||
|
|
||||||
private static AppNotificationBuilder CreateBuilder(AppNotificationScenario scenario = AppNotificationScenario.Default)
|
private static ToastContentBuilder CreateBuilder(ToastScenario scenario = ToastScenario.Default)
|
||||||
=> new AppNotificationBuilder().SetScenario(scenario);
|
=> new ToastContentBuilder().SetToastScenario(scenario);
|
||||||
|
|
||||||
private static void ShowNotification(AppNotificationBuilder builder, string? tag = null)
|
private static void ShowNotification(ToastContentBuilder builder, WinoApplicationMode mode, string? tag = null)
|
||||||
{
|
{
|
||||||
var notification = builder.BuildNotification();
|
var notification = new ToastNotification(builder.GetXml());
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tag))
|
if (!string.IsNullOrWhiteSpace(tag))
|
||||||
{
|
{
|
||||||
notification.Tag = tag;
|
notification.Tag = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppNotificationManager.Default.Show(notification);
|
ToastNotificationManager.CreateToastNotifier(AppEntryConstants.GetAppUserModelId(mode)).Show(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri GetNotificationIconUri(string iconName)
|
private static Uri GetNotificationIconUri(string iconName)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
using Wino.Mail.ViewModels;
|
|
||||||
using Wino.Mail.WinUI;
|
|
||||||
|
|
||||||
namespace Wino.Views.Abstract;
|
|
||||||
|
|
||||||
public abstract class MailCategoryManagementPageAbstract : BasePage<MailCategoryManagementPageViewModel> { }
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -210,12 +210,12 @@
|
|||||||
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||||
|
|
||||||
<!-- Test Notification -->
|
<!-- Test Notification -->
|
||||||
<!--<Button Command="{x:Bind ViewModel.CreateTestNotificationCommand}" Style="{StaticResource TransparentActionButtonStyle}">
|
<Button Command="{x:Bind ViewModel.CreateTestNotificationCommand}" Style="{StaticResource TransparentActionButtonStyle}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
||||||
<TextBlock VerticalAlignment="Center" Text="Test notification" />
|
<TextBlock VerticalAlignment="Center" Text="Test notification" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>-->
|
</Button>
|
||||||
|
|
||||||
<!-- Edit Series -->
|
<!-- Edit Series -->
|
||||||
<Border
|
<Border
|
||||||
|
|||||||
@@ -38,10 +38,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
IPopoutClient,
|
IPopoutClient,
|
||||||
IRecipient<ApplicationThemeChanged>
|
IRecipient<ApplicationThemeChanged>
|
||||||
{
|
{
|
||||||
private const int InitialFocusRetryCount = 3;
|
|
||||||
|
|
||||||
private bool _isPoppedOut;
|
private bool _isPoppedOut;
|
||||||
private bool _isInitialFocusHandled;
|
|
||||||
|
|
||||||
public bool SupportsPopOut => !_isPoppedOut;
|
public bool SupportsPopOut => !_isPoppedOut;
|
||||||
public event EventHandler<PopOutRequestedEventArgs>? PopOutRequested;
|
public event EventHandler<PopOutRequestedEventArgs>? PopOutRequested;
|
||||||
@@ -310,7 +307,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
_disposables.Add(WebViewEditor);
|
_disposables.Add(WebViewEditor);
|
||||||
|
|
||||||
ViewModel.GetHTMLBodyFunction = WebViewEditor.GetHtmlBodyAsync;
|
ViewModel.GetHTMLBodyFunction = WebViewEditor.GetHtmlBodyAsync;
|
||||||
ViewModel.RenderHtmlBodyAsyncFunc = RenderComposeHtmlAsync;
|
ViewModel.RenderHtmlBodyAsyncFunc = WebViewEditor.RenderHtmlAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowCCBCCClicked(object sender, RoutedEventArgs e)
|
private void ShowCCBCCClicked(object sender, RoutedEventArgs e)
|
||||||
@@ -376,10 +373,9 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
{
|
{
|
||||||
if (draftMailItemViewModel == null || !draftMailItemViewModel.IsDraft) return;
|
if (draftMailItemViewModel == null || !draftMailItemViewModel.IsDraft) return;
|
||||||
|
|
||||||
// Reset the initial focus flag for the newly loaded draft.
|
// Reset the initial focus flag so ToBox gets focus for the new draft.
|
||||||
_isInitialFocusHandled = false;
|
isInitialFocusHandled = false;
|
||||||
await ViewModel.RefreshDraftAsync(draftMailItemViewModel);
|
await ViewModel.RefreshDraftAsync(draftMailItemViewModel);
|
||||||
await ApplyInitialFocusAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportanceClicked(object sender, RoutedEventArgs e)
|
private void ImportanceClicked(object sender, RoutedEventArgs e)
|
||||||
@@ -438,19 +434,21 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hack: Tokenizing text box losing focus somehow on page Loaded and shifting focus to this element.
|
||||||
|
// For once we'll switch back to it once CCBBCGotFocus element got focus.
|
||||||
|
|
||||||
|
private bool isInitialFocusHandled = false;
|
||||||
|
|
||||||
private void ComposerLoaded(object sender, RoutedEventArgs e)
|
private void ComposerLoaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (ShouldFocusRecipients())
|
ToBox.Focus(FocusState.Programmatic);
|
||||||
{
|
|
||||||
ToBox.Focus(FocusState.Programmatic);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CCBBCGotFocus(object sender, RoutedEventArgs e)
|
private void CCBBCGotFocus(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (ShouldFocusRecipients() && !_isInitialFocusHandled)
|
if (!isInitialFocusHandled)
|
||||||
{
|
{
|
||||||
_isInitialFocusHandled = true;
|
isInitialFocusHandled = true;
|
||||||
ToBox.Focus(FocusState.Programmatic);
|
ToBox.Focus(FocusState.Programmatic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,64 +555,4 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
}
|
}
|
||||||
finally { deferral.Complete(); }
|
finally { deferral.Complete(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldFocusRecipients()
|
|
||||||
=> !ShouldFocusEditor();
|
|
||||||
|
|
||||||
private bool ShouldFocusEditor()
|
|
||||||
{
|
|
||||||
var inReplyTo = ViewModel.CurrentMimeMessage?.InReplyTo;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(inReplyTo))
|
|
||||||
{
|
|
||||||
inReplyTo = ViewModel.CurrentMailDraftItem?.MailCopy?.InReplyTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(inReplyTo) && ViewModel.CurrentMimeMessage?.Headers.Contains(HeaderId.InReplyTo) == true)
|
|
||||||
{
|
|
||||||
inReplyTo = ViewModel.CurrentMimeMessage.Headers[HeaderId.InReplyTo];
|
|
||||||
}
|
|
||||||
|
|
||||||
return !string.IsNullOrWhiteSpace(inReplyTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyInitialFocusAsync()
|
|
||||||
{
|
|
||||||
if (_isInitialFocusHandled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isInitialFocusHandled = true;
|
|
||||||
|
|
||||||
for (var attempt = 0; attempt < InitialFocusRetryCount; attempt++)
|
|
||||||
{
|
|
||||||
if (ShouldFocusEditor())
|
|
||||||
{
|
|
||||||
await WebViewEditor.FocusEditorAsync(true);
|
|
||||||
|
|
||||||
if (FocusManager.GetFocusedElement(XamlRoot) is WebView2)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ToBox.Focus(FocusState.Programmatic);
|
|
||||||
|
|
||||||
if (FocusManager.GetFocusedElement(XamlRoot) == ToBox)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(50));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RenderComposeHtmlAsync(string html)
|
|
||||||
{
|
|
||||||
await WebViewEditor.RenderHtmlAsync(html);
|
|
||||||
await ApplyInitialFocusAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,8 +253,7 @@
|
|||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Command="{x:Bind ViewModel.SyncFolderCommand}"
|
Command="{x:Bind ViewModel.SyncFolderCommand}"
|
||||||
IsEnabled="{x:Bind ViewModel.CanSynchronize, Mode=OneWay}"
|
IsEnabled="{x:Bind ViewModel.CanSynchronize, Mode=OneWay}"
|
||||||
ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_Sync}"
|
ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_Sync}">
|
||||||
Visibility="{x:Bind ViewModel.IsSyncButtonVisible, Mode=OneWay}">
|
|
||||||
<coreControls:WinoFontIcon FontSize="14" Icon="Sync" />
|
<coreControls:WinoFontIcon FontSize="14" Icon="Sync" />
|
||||||
</Button>
|
</Button>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
@@ -291,11 +290,9 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<InfoBar
|
<InfoBar
|
||||||
x:Name="SyncDisabledInfoBar"
|
|
||||||
Title="{x:Bind domain:Translator.InfoBarTitle_SynchronizationDisabledFolder}"
|
Title="{x:Bind domain:Translator.InfoBarTitle_SynchronizationDisabledFolder}"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.ColumnSpan="3"
|
Grid.ColumnSpan="3"
|
||||||
x:Load="{x:Bind ViewModel.IsSyncButtonVisible, Mode=OneWay}"
|
|
||||||
IsClosable="True"
|
IsClosable="True"
|
||||||
IsOpen="{x:Bind ViewModel.IsFolderSynchronizationEnabled, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}"
|
IsOpen="{x:Bind ViewModel.IsFolderSynchronizationEnabled, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}"
|
||||||
Message="{x:Bind domain:Translator.InfoBarMessage_SynchronizationDisabledFolder}"
|
Message="{x:Bind domain:Translator.InfoBarMessage_SynchronizationDisabledFolder}"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Collections;
|
using CommunityToolkit.Mvvm.Collections;
|
||||||
@@ -18,7 +18,6 @@ using Windows.Foundation;
|
|||||||
using Windows.System;
|
using Windows.System;
|
||||||
using Wino.Controls;
|
using Wino.Controls;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
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;
|
||||||
@@ -28,6 +27,7 @@ using Wino.Mail.ViewModels.Data;
|
|||||||
using Wino.Mail.ViewModels.Messages;
|
using Wino.Mail.ViewModels.Messages;
|
||||||
using Wino.Mail.WinUI;
|
using Wino.Mail.WinUI;
|
||||||
using Wino.Mail.WinUI.Controls.ListView;
|
using Wino.Mail.WinUI.Controls.ListView;
|
||||||
|
using Wino.Mail.WinUI.Extensions;
|
||||||
using Wino.Mail.WinUI.Helpers;
|
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;
|
||||||
@@ -246,27 +246,14 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
// Default to all selected items.
|
// Default to all selected items.
|
||||||
targetItems = ViewModel.MailCollection.SelectedItems;
|
targetItems = ViewModel.MailCollection.SelectedItems;
|
||||||
var availableActions = ViewModel.GetAvailableMailActions(targetItems);
|
var availableActions = ViewModel.GetAvailableMailActions(targetItems);
|
||||||
var (availableCategories, assignedCategoryIds) = await ViewModel.GetAvailableCategoriesAsync(targetItems);
|
|
||||||
|
|
||||||
if (availableActions == null || !availableActions.Any()) return;
|
if (availableActions == null || !availableActions.Any()) return;
|
||||||
|
|
||||||
var clickedAction = await GetMailContextActionFromFlyoutAsync(
|
var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
|
||||||
availableActions,
|
|
||||||
availableCategories,
|
|
||||||
assignedCategoryIds,
|
|
||||||
control,
|
|
||||||
p.X,
|
|
||||||
p.Y);
|
|
||||||
|
|
||||||
if (clickedAction == null) return;
|
if (clickedOperation == null) return;
|
||||||
|
|
||||||
if (clickedAction.Category != null)
|
var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
|
||||||
{
|
|
||||||
await ViewModel.ToggleCategoryAssignmentAsync(clickedAction.Category, targetItems, clickedAction.IsCategoryAssignedToAll);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prepRequest = new MailOperationPreperationRequest(clickedAction.Operation.Operation, targetItems.Select(a => a.MailCopy));
|
|
||||||
|
|
||||||
await ViewModel.ExecuteMailOperationAsync(prepRequest);
|
await ViewModel.ExecuteMailOperationAsync(prepRequest);
|
||||||
}
|
}
|
||||||
@@ -309,68 +296,14 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<MailContextAction?> GetMailContextActionFromFlyoutAsync(
|
private async Task<MailOperationMenuItem> GetMailOperationFromFlyoutAsync(IEnumerable<MailOperationMenuItem> availableActions,
|
||||||
IEnumerable<MailOperationMenuItem> availableActions,
|
UIElement showAtElement,
|
||||||
IReadOnlyList<MailCategory> availableCategories,
|
double x,
|
||||||
IReadOnlyCollection<Guid> assignedCategoryIds,
|
double y)
|
||||||
UIElement showAtElement,
|
|
||||||
double x,
|
|
||||||
double y)
|
|
||||||
{
|
{
|
||||||
var source = new TaskCompletionSource<MailContextAction?>();
|
var source = new TaskCompletionSource<MailOperationMenuItem>();
|
||||||
var flyout = new MenuFlyout();
|
|
||||||
|
|
||||||
foreach (var action in availableActions)
|
var flyout = new MailOperationFlyout(availableActions, source);
|
||||||
{
|
|
||||||
if (action.Operation == MailOperation.Seperator)
|
|
||||||
{
|
|
||||||
flyout.Items.Add(new MenuFlyoutSeparator());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var menuFlyoutItem = new MailOperationMenuFlyoutItem(action, clicked =>
|
|
||||||
{
|
|
||||||
source.TrySetResult(new MailContextAction(clicked));
|
|
||||||
flyout.Hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
flyout.Items.Add(menuFlyoutItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (availableCategories?.Count > 0)
|
|
||||||
{
|
|
||||||
if (flyout.Items.LastOrDefault() is not MenuFlyoutSeparator)
|
|
||||||
{
|
|
||||||
flyout.Items.Add(new MenuFlyoutSeparator());
|
|
||||||
}
|
|
||||||
|
|
||||||
var categorySubItem = new MenuFlyoutSubItem
|
|
||||||
{
|
|
||||||
Text = Translator.MailCategoryMenuItem
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var category in availableCategories)
|
|
||||||
{
|
|
||||||
var wasAssignedToAll = assignedCategoryIds.Contains(category.Id);
|
|
||||||
var categoryItem = new ToggleMenuFlyoutItem
|
|
||||||
{
|
|
||||||
Text = category.Name,
|
|
||||||
IsChecked = wasAssignedToAll
|
|
||||||
};
|
|
||||||
|
|
||||||
categoryItem.Click += (_, _) =>
|
|
||||||
{
|
|
||||||
source.TrySetResult(new MailContextAction(category, wasAssignedToAll));
|
|
||||||
flyout.Hide();
|
|
||||||
};
|
|
||||||
|
|
||||||
categorySubItem.Items.Add(categoryItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
flyout.Items.Add(categorySubItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
flyout.Closing += (_, _) => source.TrySetResult(null);
|
|
||||||
|
|
||||||
flyout.ShowAt(showAtElement, new FlyoutShowOptions()
|
flyout.ShowAt(showAtElement, new FlyoutShowOptions()
|
||||||
{
|
{
|
||||||
@@ -381,13 +314,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
return await source.Task;
|
return await source.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record MailContextAction(MailOperationMenuItem Operation, MailCategory Category = null, bool IsCategoryAssignedToAll = false)
|
|
||||||
{
|
|
||||||
public MailContextAction(MailCategory category, bool isCategoryAssignedToAll) : this(null, category, isCategoryAssignedToAll)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async void IRecipient<ClearMailSelectionsRequested>.Receive(ClearMailSelectionsRequested message)
|
async void IRecipient<ClearMailSelectionsRequested>.Receive(ClearMailSelectionsRequested message)
|
||||||
{
|
{
|
||||||
await ViewModel.MailCollection.UnselectAllAsync();
|
await ViewModel.MailCollection.UnselectAllAsync();
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
<abstract:MailCategoryManagementPageAbstract
|
|
||||||
x:Class="Wino.Views.Settings.MailCategoryManagementPage"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:abstract="using:Wino.Views.Abstract"
|
|
||||||
xmlns:controls="using:Wino.Controls"
|
|
||||||
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
|
||||||
xmlns:mail="using:Wino.Core.Domain.Entities.Mail"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
x:Name="root"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
|
|
||||||
<Page.Resources>
|
|
||||||
<DataTemplate x:Key="MailCategoryTemplate" x:DataType="mail:MailCategory">
|
|
||||||
<Grid
|
|
||||||
Margin="0,0,0,12"
|
|
||||||
Padding="0,0,0,12"
|
|
||||||
ColumnSpacing="16">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
ColumnSpacing="12">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Border
|
|
||||||
Width="28"
|
|
||||||
Height="28"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}"
|
|
||||||
BorderBrush="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="8" />
|
|
||||||
|
|
||||||
<StackPanel
|
|
||||||
Grid.Column="1"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Spacing="2">
|
|
||||||
<TextBlock
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
Style="{StaticResource BodyTextBlockStyle}"
|
|
||||||
Text="{x:Bind Name}"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
<TextBlock
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Style="{StaticResource CaptionTextBlockStyle}">
|
|
||||||
<Run Text="{x:Bind BackgroundColorHex}" />
|
|
||||||
<Run Text=" / " />
|
|
||||||
<Run Text="{x:Bind TextColorHex}" />
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ToggleButton
|
|
||||||
Grid.Column="1"
|
|
||||||
Checked="FavoriteCategoryChecked"
|
|
||||||
IsChecked="{x:Bind IsFavorite, Mode=OneWay}"
|
|
||||||
Tag="{x:Bind}"
|
|
||||||
Unchecked="FavoriteCategoryUnchecked">
|
|
||||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
|
||||||
</ToggleButton>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
Grid.Column="2"
|
|
||||||
Click="EditCategoryClicked"
|
|
||||||
Tag="{x:Bind}">
|
|
||||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
Grid.Column="3"
|
|
||||||
Click="DeleteCategoryClicked"
|
|
||||||
Tag="{x:Bind}">
|
|
||||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Rectangle
|
|
||||||
Grid.ColumnSpan="5"
|
|
||||||
Height="1"
|
|
||||||
Margin="0,12,0,0"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</Page.Resources>
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
ItemTemplate="{StaticResource MailCategoryTemplate}"
|
|
||||||
ItemsSource="{x:Bind ViewModel.Categories, Mode=OneWay}"
|
|
||||||
SelectionMode="None">
|
|
||||||
<ListView.ItemContainerTransitions>
|
|
||||||
<TransitionCollection>
|
|
||||||
<NavigationThemeTransition />
|
|
||||||
</TransitionCollection>
|
|
||||||
</ListView.ItemContainerTransitions>
|
|
||||||
<ListView.Header>
|
|
||||||
<Grid
|
|
||||||
Padding="16,0,24,20"
|
|
||||||
ColumnSpacing="16">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Grid Style="{StaticResource InformationAreaGridStyle}">
|
|
||||||
<TextBlock HorizontalTextAlignment="Center" TextWrapping="WrapWholeWords">
|
|
||||||
<Run FontWeight="SemiBold" Text="{x:Bind domain:Translator.MailCategoryManagementPage_Title, Mode=OneTime}" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="{x:Bind domain:Translator.MailCategoryManagementPage_Description, Mode=OneTime}" />
|
|
||||||
</TextBlock>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
Grid.Column="1"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
Command="{x:Bind ViewModel.RefreshCategoriesCommand}"
|
|
||||||
Visibility="{x:Bind ViewModel.CanRefresh, Mode=OneWay}">
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<FontIcon
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Glyph="" />
|
|
||||||
<TextBlock
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Text="{x:Bind domain:Translator.Buttons_Refresh, Mode=OneTime}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
Grid.Column="2"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
Command="{x:Bind ViewModel.AddCategoryCommand}">
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<FontIcon
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Glyph="" />
|
|
||||||
<TextBlock
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Text="{x:Bind domain:Translator.Buttons_Add, Mode=OneTime}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</ListView.Header>
|
|
||||||
<ListView.Footer>
|
|
||||||
<TextBlock
|
|
||||||
Margin="0,12,0,0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Style="{StaticResource BodyTextBlockStyle}"
|
|
||||||
Text="{x:Bind domain:Translator.MailCategoryManagementPage_Empty, Mode=OneTime}"
|
|
||||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.HasCategories), Mode=OneWay}" />
|
|
||||||
</ListView.Footer>
|
|
||||||
</ListView>
|
|
||||||
</abstract:MailCategoryManagementPageAbstract>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Views.Abstract;
|
|
||||||
|
|
||||||
namespace Wino.Views.Settings;
|
|
||||||
|
|
||||||
public sealed partial class MailCategoryManagementPage : MailCategoryManagementPageAbstract
|
|
||||||
{
|
|
||||||
public MailCategoryManagementPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void FavoriteCategoryChecked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is ToggleButton toggleButton && toggleButton.Tag is MailCategory category)
|
|
||||||
{
|
|
||||||
await ViewModel.SetFavoriteAsync(category, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void FavoriteCategoryUnchecked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is ToggleButton toggleButton && toggleButton.Tag is MailCategory category)
|
|
||||||
{
|
|
||||||
await ViewModel.SetFavoriteAsync(category, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void EditCategoryClicked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is Button button && button.Tag is MailCategory category)
|
|
||||||
{
|
|
||||||
await ViewModel.EditCategoryAsync(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void DeleteCategoryClicked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is Button button && button.Tag is MailCategory category)
|
|
||||||
{
|
|
||||||
await ViewModel.DeleteCategoryAsync(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -240,93 +240,6 @@
|
|||||||
</coreControls:WinoNavigationViewItem>
|
</coreControls:WinoNavigationViewItem>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="MailCategoryMenuTemplate" x:DataType="menu:MailCategoryMenuItem">
|
|
||||||
<coreControls:WinoNavigationViewItem
|
|
||||||
MinHeight="40"
|
|
||||||
DataContext="{x:Bind}"
|
|
||||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsSelected), Mode=OneWay}"
|
|
||||||
IsSelected="{x:Bind IsSelected, Mode=TwoWay}"
|
|
||||||
SelectsOnInvoked="True"
|
|
||||||
ToolTipService.ToolTip="{x:Bind FolderName, Mode=OneWay}">
|
|
||||||
<muxc:NavigationViewItem.Icon>
|
|
||||||
<FontIcon
|
|
||||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
|
||||||
Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}"
|
|
||||||
Glyph="" />
|
|
||||||
</muxc:NavigationViewItem.Icon>
|
|
||||||
<muxc:NavigationViewItem.InfoBadge>
|
|
||||||
<muxc:InfoBadge
|
|
||||||
Background="{StaticResource SystemAccentColor}"
|
|
||||||
Foreground="White"
|
|
||||||
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(UnreadItemCount), Mode=OneWay}"
|
|
||||||
Value="{x:Bind UnreadItemCount, Mode=OneWay}" />
|
|
||||||
</muxc:NavigationViewItem.InfoBadge>
|
|
||||||
<muxc:NavigationViewItem.Content>
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="1"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightBySyncState(IsSelected), Mode=OneWay}"
|
|
||||||
Style="{StaticResource BodyTextBlockStyle}"
|
|
||||||
Text="{x:Bind FolderName, Mode=OneWay}"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
</muxc:NavigationViewItem.Content>
|
|
||||||
</coreControls:WinoNavigationViewItem>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<DataTemplate x:Key="MergedMailCategoryMenuTemplate" x:DataType="menu:MergedMailCategoryMenuItem">
|
|
||||||
<coreControls:WinoNavigationViewItem
|
|
||||||
MinHeight="40"
|
|
||||||
DataContext="{x:Bind}"
|
|
||||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsSelected), Mode=OneWay}"
|
|
||||||
IsSelected="{x:Bind IsSelected, Mode=TwoWay}"
|
|
||||||
SelectsOnInvoked="True"
|
|
||||||
ToolTipService.ToolTip="{x:Bind FolderName, Mode=OneWay}">
|
|
||||||
<muxc:NavigationViewItem.Icon>
|
|
||||||
<FontIcon
|
|
||||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
|
||||||
Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
Glyph="" />
|
|
||||||
</muxc:NavigationViewItem.Icon>
|
|
||||||
<muxc:NavigationViewItem.InfoBadge>
|
|
||||||
<muxc:InfoBadge
|
|
||||||
Background="{StaticResource SystemAccentColor}"
|
|
||||||
Foreground="White"
|
|
||||||
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(UnreadItemCount), Mode=OneWay}"
|
|
||||||
Value="{x:Bind UnreadItemCount, Mode=OneWay}" />
|
|
||||||
</muxc:NavigationViewItem.InfoBadge>
|
|
||||||
<muxc:NavigationViewItem.Content>
|
|
||||||
<Grid
|
|
||||||
MaxHeight="36"
|
|
||||||
Padding="2"
|
|
||||||
VerticalAlignment="Center">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Border
|
|
||||||
Width="10"
|
|
||||||
Height="10"
|
|
||||||
Margin="0,0,8,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}"
|
|
||||||
BorderBrush="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="3" />
|
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="1"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightBySyncState(IsSelected), Mode=OneWay}"
|
|
||||||
Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
|
|
||||||
Style="{StaticResource BodyTextBlockStyle}"
|
|
||||||
Text="{x:Bind FolderName, Mode=OneWay}"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
</Grid>
|
|
||||||
</muxc:NavigationViewItem.Content>
|
|
||||||
</coreControls:WinoNavigationViewItem>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<!-- Merged Inbox -->
|
<!-- Merged Inbox -->
|
||||||
<DataTemplate x:Key="MergedAccountTemplate" x:DataType="menu:MergedAccountMenuItem">
|
<DataTemplate x:Key="MergedAccountTemplate" x:DataType="menu:MergedAccountMenuItem">
|
||||||
<controls:AccountNavigationItem
|
<controls:AccountNavigationItem
|
||||||
@@ -480,7 +393,6 @@
|
|||||||
|
|
||||||
<coreSelectors:NavigationMenuTemplateSelector
|
<coreSelectors:NavigationMenuTemplateSelector
|
||||||
x:Key="NavigationMenuTemplateSelector"
|
x:Key="NavigationMenuTemplateSelector"
|
||||||
CategoryItemsTemplate="{StaticResource MailCategoryMenuTemplate}"
|
|
||||||
ClickableAccountMenuTemplate="{StaticResource ClickableAccountMenuTemplate}"
|
ClickableAccountMenuTemplate="{StaticResource ClickableAccountMenuTemplate}"
|
||||||
FixAuthenticationIssueTemplate="{StaticResource FixAuthenticationIssueTemplate}"
|
FixAuthenticationIssueTemplate="{StaticResource FixAuthenticationIssueTemplate}"
|
||||||
FixMissingFolderConfigTemplate="{StaticResource FixMissingFolderConfig}"
|
FixMissingFolderConfigTemplate="{StaticResource FixMissingFolderConfig}"
|
||||||
@@ -488,7 +400,6 @@
|
|||||||
MergedAccountFolderTemplate="{StaticResource MergedAccountFolderMenuItemTemplate}"
|
MergedAccountFolderTemplate="{StaticResource MergedAccountFolderMenuItemTemplate}"
|
||||||
MergedAccountMoreExpansionItemTemplate="{StaticResource MergedAccountMoreFolderItemTemplate}"
|
MergedAccountMoreExpansionItemTemplate="{StaticResource MergedAccountMoreFolderItemTemplate}"
|
||||||
MergedAccountTemplate="{StaticResource MergedAccountTemplate}"
|
MergedAccountTemplate="{StaticResource MergedAccountTemplate}"
|
||||||
MergedCategoryItemsTemplate="{StaticResource MergedMailCategoryMenuTemplate}"
|
|
||||||
NewMailTemplate="{StaticResource CreateNewMailTemplate}"
|
NewMailTemplate="{StaticResource CreateNewMailTemplate}"
|
||||||
RatingItemTemplate="{StaticResource RatingItemTemplate}"
|
RatingItemTemplate="{StaticResource RatingItemTemplate}"
|
||||||
SeperatorTemplate="{StaticResource SeperatorTemplate}"
|
SeperatorTemplate="{StaticResource SeperatorTemplate}"
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Lottie" />
|
<PackageReference Include="CommunityToolkit.WinUI.Lottie" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ public class DatabaseService : IDatabaseService
|
|||||||
{
|
{
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
Connection.CreateTableAsync<MailCopy>(),
|
Connection.CreateTableAsync<MailCopy>(),
|
||||||
Connection.CreateTableAsync<MailCategory>(),
|
|
||||||
Connection.CreateTableAsync<MailCategoryAssignment>(),
|
|
||||||
Connection.CreateTableAsync<MailItemFolder>(),
|
Connection.CreateTableAsync<MailItemFolder>(),
|
||||||
Connection.CreateTableAsync<MailAccount>(),
|
Connection.CreateTableAsync<MailAccount>(),
|
||||||
Connection.CreateTableAsync<AccountContact>(),
|
Connection.CreateTableAsync<AccountContact>(),
|
||||||
@@ -170,13 +168,6 @@ public class DatabaseService : IDatabaseService
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accountCalendarColumns.Any(c => c.Name == nameof(AccountCalendar.IsReadOnly)))
|
|
||||||
{
|
|
||||||
await Connection
|
|
||||||
.ExecuteAsync($"ALTER TABLE {nameof(AccountCalendar)} ADD COLUMN {nameof(AccountCalendar.IsReadOnly)} INTEGER NOT NULL DEFAULT 0")
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Connection.ExecuteAsync("DROP TABLE IF EXISTS WinoAccountAddOnCache").ConfigureAwait(false);
|
await Connection.ExecuteAsync("DROP TABLE IF EXISTS WinoAccountAddOnCache").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,12 +219,6 @@ SET {nameof(KeyboardShortcut.Action)} =
|
|||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_MessageId ON MailCopy(MessageId)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_MessageId ON MailCopy(MessageId)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_FolderId_IsRead ON MailCopy(FolderId, IsRead)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_FolderId_IsRead ON MailCopy(FolderId, IsRead)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_CreationDate ON MailCopy(CreationDate)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_CreationDate ON MailCopy(CreationDate)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId ON MailCategory(MailAccountId)").ConfigureAwait(false);
|
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId_Name ON MailCategory(MailAccountId, Name)").ConfigureAwait(false);
|
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId_IsFavorite ON MailCategory(MailAccountId, IsFavorite)").ConfigureAwait(false);
|
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategoryAssignment_MailCategoryId ON MailCategoryAssignment(MailCategoryId)").ConfigureAwait(false);
|
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategoryAssignment_MailCopyUniqueId ON MailCategoryAssignment(MailCopyUniqueId)").ConfigureAwait(false);
|
|
||||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailCategoryAssignment_Category_MailCopy ON MailCategoryAssignment(MailCategoryId, MailCopyUniqueId)").ConfigureAwait(false);
|
|
||||||
|
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId ON MailItemFolder(MailAccountId)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId ON MailItemFolder(MailAccountId)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId_RemoteFolderId ON MailItemFolder(MailAccountId, RemoteFolderId)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId_RemoteFolderId ON MailItemFolder(MailAccountId, RemoteFolderId)").ConfigureAwait(false);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ namespace Wino.Services;
|
|||||||
public class FolderService : BaseDatabaseService, IFolderService
|
public class FolderService : BaseDatabaseService, IFolderService
|
||||||
{
|
{
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
private readonly ILogger _logger = Log.ForContext<FolderService>();
|
private readonly ILogger _logger = Log.ForContext<FolderService>();
|
||||||
|
|
||||||
private readonly SpecialFolderType[] gmailCategoryFolderTypes =
|
private readonly SpecialFolderType[] gmailCategoryFolderTypes =
|
||||||
@@ -35,11 +34,9 @@ public class FolderService : BaseDatabaseService, IFolderService
|
|||||||
];
|
];
|
||||||
|
|
||||||
public FolderService(IDatabaseService databaseService,
|
public FolderService(IDatabaseService databaseService,
|
||||||
IAccountService accountService,
|
IAccountService accountService) : base(databaseService)
|
||||||
IMailCategoryService mailCategoryService) : base(databaseService)
|
|
||||||
{
|
{
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
|
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
|
||||||
@@ -272,9 +269,6 @@ public class FolderService : BaseDatabaseService, IFolderService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var favoriteCategories = await GetFavoriteCategoryMenuItemsAsync(mailAccount, folders, accountMenuItem).ConfigureAwait(false);
|
|
||||||
preparedFolderMenuItems.AddRange(favoriteCategories);
|
|
||||||
|
|
||||||
// Only add category folder if it's Gmail.
|
// Only add category folder if it's Gmail.
|
||||||
if (mailAccount.ProviderType == MailProviderType.Gmail) preparedFolderMenuItems.Add(categoryFolderMenuItem);
|
if (mailAccount.ProviderType == MailProviderType.Gmail) preparedFolderMenuItems.Add(categoryFolderMenuItem);
|
||||||
|
|
||||||
@@ -315,62 +309,9 @@ public class FolderService : BaseDatabaseService, IFolderService
|
|||||||
preparedFolderMenuItems.Add(menuItem);
|
preparedFolderMenuItems.Add(menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
var favoriteCategories = await GetMergedFavoriteCategoryMenuItemsAsync(holdingAccounts, allAccountFolders, mergedAccountFolderMenuItem.Parameter).ConfigureAwait(false);
|
|
||||||
preparedFolderMenuItems.AddRange(favoriteCategories);
|
|
||||||
|
|
||||||
return preparedFolderMenuItems;
|
return preparedFolderMenuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<IMenuItem>> GetFavoriteCategoryMenuItemsAsync(MailAccount account, IEnumerable<IMailItemFolder> handlingFolders, IMenuItem parentMenuItem)
|
|
||||||
{
|
|
||||||
var favoriteCategories = await _mailCategoryService.GetFavoriteCategoriesAsync(account.Id).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!favoriteCategories.Any())
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var availableFolders = handlingFolders
|
|
||||||
.Where(a => a.IsMoveTarget)
|
|
||||||
.Cast<IMailItemFolder>()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return favoriteCategories
|
|
||||||
.Select(category => (IMenuItem)new MailCategoryMenuItem(category, account, availableFolders, parentMenuItem))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IEnumerable<IMenuItem>> GetMergedFavoriteCategoryMenuItemsAsync(IEnumerable<MailAccount> holdingAccounts, IEnumerable<IEnumerable<MailItemFolder>> allAccountFolders, MergedInbox mergedInbox)
|
|
||||||
{
|
|
||||||
var categoriesByAccount = new List<(MailAccount Account, List<MailCategory> Categories)>();
|
|
||||||
|
|
||||||
foreach (var account in holdingAccounts)
|
|
||||||
{
|
|
||||||
var categories = await _mailCategoryService.GetFavoriteCategoriesAsync(account.Id).ConfigureAwait(false);
|
|
||||||
if (categories.Any())
|
|
||||||
{
|
|
||||||
categoriesByAccount.Add((account, categories));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!categoriesByAccount.Any())
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var handlingFolders = allAccountFolders
|
|
||||||
.SelectMany(a => a)
|
|
||||||
.Where(a => a.IsMoveTarget)
|
|
||||||
.Cast<IMailItemFolder>()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return categoriesByAccount
|
|
||||||
.SelectMany(a => a.Categories)
|
|
||||||
.GroupBy(a => NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(group => (IMenuItem)new MergedMailCategoryMenuItem(group.ToList(), handlingFolders, mergedInbox))
|
|
||||||
.OrderBy(item => ((MergedMailCategoryMenuItem)item).FolderName, StringComparer.CurrentCultureIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeCategoryName(string name)
|
|
||||||
=> name?.Trim() ?? string.Empty;
|
|
||||||
|
|
||||||
private HashSet<SpecialFolderType> FindCommonFolders(List<List<MailItemFolder>> lists)
|
private HashSet<SpecialFolderType> FindCommonFolders(List<List<MailItemFolder>> lists)
|
||||||
{
|
{
|
||||||
var allSpecialTypesExceptOther = Enum.GetValues<SpecialFolderType>().Cast<SpecialFolderType>().Where(a => a != SpecialFolderType.Other).ToList();
|
var allSpecialTypesExceptOther = Enum.GetValues<SpecialFolderType>().Cast<SpecialFolderType>().Where(a => a != SpecialFolderType.Other).ToList();
|
||||||
|
|||||||
@@ -1,358 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
|
||||||
using Wino.Messaging.Client.Accounts;
|
|
||||||
using Wino.Messaging.UI;
|
|
||||||
|
|
||||||
namespace Wino.Services;
|
|
||||||
|
|
||||||
public class MailCategoryService : BaseDatabaseService, IMailCategoryService
|
|
||||||
{
|
|
||||||
public MailCategoryService(IDatabaseService databaseService) : base(databaseService)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<MailCategory>> GetCategoriesAsync(Guid accountId)
|
|
||||||
=> Connection.QueryAsync<MailCategory>(
|
|
||||||
$"SELECT * FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? ORDER BY {nameof(MailCategory.IsFavorite)} DESC, {nameof(MailCategory.Name)} COLLATE NOCASE",
|
|
||||||
accountId);
|
|
||||||
|
|
||||||
public Task<List<MailCategory>> GetFavoriteCategoriesAsync(Guid accountId)
|
|
||||||
=> Connection.QueryAsync<MailCategory>(
|
|
||||||
$"SELECT * FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? AND {nameof(MailCategory.IsFavorite)} = 1 ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE",
|
|
||||||
accountId);
|
|
||||||
|
|
||||||
public Task<MailCategory> GetCategoryAsync(Guid categoryId)
|
|
||||||
=> Connection.FindAsync<MailCategory>(categoryId);
|
|
||||||
|
|
||||||
public async Task<bool> CategoryNameExistsAsync(Guid accountId, string name, Guid? excludedCategoryId = null)
|
|
||||||
{
|
|
||||||
var normalizedName = NormalizeCategoryName(name);
|
|
||||||
if (string.IsNullOrWhiteSpace(normalizedName))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var sql = $"SELECT COUNT(*) FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? AND lower(trim({nameof(MailCategory.Name)})) = ?";
|
|
||||||
var parameters = new List<object> { accountId, normalizedName.ToLowerInvariant() };
|
|
||||||
|
|
||||||
if (excludedCategoryId.HasValue)
|
|
||||||
{
|
|
||||||
sql += $" AND {nameof(MailCategory.Id)} <> ?";
|
|
||||||
parameters.Add(excludedCategoryId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await Connection.ExecuteScalarAsync<int>(sql, parameters.ToArray()).ConfigureAwait(false) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MailCategory> CreateCategoryAsync(MailCategory category)
|
|
||||||
{
|
|
||||||
category.Id = category.Id == Guid.Empty ? Guid.NewGuid() : category.Id;
|
|
||||||
category.Name = NormalizeCategoryName(category.Name);
|
|
||||||
|
|
||||||
await Connection.InsertAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
|
||||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
|
||||||
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateCategoryAsync(MailCategory category)
|
|
||||||
{
|
|
||||||
category.Name = NormalizeCategoryName(category.Name);
|
|
||||||
|
|
||||||
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
|
||||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteCategoryAsync(Guid categoryId)
|
|
||||||
{
|
|
||||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
|
||||||
if (category == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await Connection.ExecuteAsync($"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ?", categoryId).ConfigureAwait(false);
|
|
||||||
await Connection.DeleteAsync<MailCategory>(categoryId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteCategoriesAsync(Guid accountId)
|
|
||||||
{
|
|
||||||
var categories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (categories.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var categoryIds = categories.Select(a => a.Id).ToList();
|
|
||||||
var placeholders = string.Join(",", categoryIds.Select(_ => "?"));
|
|
||||||
var deleteAssignmentsSql = $"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} IN ({placeholders})";
|
|
||||||
|
|
||||||
await Connection.ExecuteAsync(deleteAssignmentsSql, categoryIds.Cast<object>().ToArray()).ConfigureAwait(false);
|
|
||||||
await Connection.Table<MailCategory>().DeleteAsync(a => a.MailAccountId == accountId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
NotifyCategoryStructureChanged(accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ToggleFavoriteAsync(Guid categoryId, bool isFavorite)
|
|
||||||
{
|
|
||||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
|
||||||
if (category == null || category.IsFavorite == isFavorite)
|
|
||||||
return;
|
|
||||||
|
|
||||||
category.IsFavorite = isFavorite;
|
|
||||||
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
NotifyCategoryStructureChanged(category.MailAccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateRemoteIdAsync(Guid categoryId, string remoteId)
|
|
||||||
{
|
|
||||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
|
||||||
if (category == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
category.RemoteId = remoteId;
|
|
||||||
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ReplaceCategoriesAsync(Guid accountId, IEnumerable<MailCategory> categories)
|
|
||||||
{
|
|
||||||
var existingCategories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
|
|
||||||
var existingByRemoteId = existingCategories
|
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a.RemoteId))
|
|
||||||
.ToDictionary(a => a.RemoteId, StringComparer.OrdinalIgnoreCase);
|
|
||||||
var existingByName = existingCategories
|
|
||||||
.GroupBy(a => NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(a => a.Key, a => a.First(), StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var incomingCategories = categories?.ToList() ?? [];
|
|
||||||
var preservedIds = new HashSet<Guid>();
|
|
||||||
|
|
||||||
foreach (var incoming in incomingCategories)
|
|
||||||
{
|
|
||||||
incoming.MailAccountId = accountId;
|
|
||||||
incoming.Id = incoming.Id == Guid.Empty ? Guid.NewGuid() : incoming.Id;
|
|
||||||
incoming.Name = NormalizeCategoryName(incoming.Name);
|
|
||||||
|
|
||||||
MailCategory existing = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(incoming.RemoteId) && existingByRemoteId.TryGetValue(incoming.RemoteId, out var byRemote))
|
|
||||||
{
|
|
||||||
existing = byRemote;
|
|
||||||
}
|
|
||||||
else if (existingByName.TryGetValue(incoming.Name, out var byName))
|
|
||||||
{
|
|
||||||
existing = byName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
await Connection.InsertAsync(incoming, typeof(MailCategory)).ConfigureAwait(false);
|
|
||||||
preservedIds.Add(incoming.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
incoming.Id = existing.Id;
|
|
||||||
incoming.IsFavorite = existing.IsFavorite;
|
|
||||||
await Connection.UpdateAsync(incoming, typeof(MailCategory)).ConfigureAwait(false);
|
|
||||||
preservedIds.Add(existing.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var categoryIdsToDelete = existingCategories
|
|
||||||
.Where(a => !preservedIds.Contains(a.Id))
|
|
||||||
.Select(a => a.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (categoryIdsToDelete.Count > 0)
|
|
||||||
{
|
|
||||||
var placeholders = string.Join(",", categoryIdsToDelete.Select(_ => "?"));
|
|
||||||
await Connection.ExecuteAsync(
|
|
||||||
$"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} IN ({placeholders})",
|
|
||||||
categoryIdsToDelete.Cast<object>().ToArray()).ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var categoryId in categoryIdsToDelete)
|
|
||||||
{
|
|
||||||
await Connection.DeleteAsync<MailCategory>(categoryId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifyCategoryStructureChanged(accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ReplaceMailAssignmentsAsync(Guid accountId, Guid mailCopyUniqueId, IEnumerable<string> categoryNames)
|
|
||||||
{
|
|
||||||
var normalizedNames = categoryNames?
|
|
||||||
.Select(NormalizeCategoryName)
|
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList() ?? [];
|
|
||||||
|
|
||||||
var availableCategories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
|
|
||||||
var categoryIds = availableCategories
|
|
||||||
.Where(a => normalizedNames.Contains(NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase))
|
|
||||||
.Select(a => a.Id)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
var existingAssignments = await Connection.QueryAsync<MailCategoryAssignment>(
|
|
||||||
$"SELECT * FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} = ?",
|
|
||||||
mailCopyUniqueId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var assignmentsToDelete = existingAssignments.Where(a => !categoryIds.Contains(a.MailCategoryId)).ToList();
|
|
||||||
var existingIds = existingAssignments.Select(a => a.MailCategoryId).ToHashSet();
|
|
||||||
var assignmentsToAdd = categoryIds.Where(a => !existingIds.Contains(a)).ToList();
|
|
||||||
|
|
||||||
foreach (var assignment in assignmentsToDelete)
|
|
||||||
{
|
|
||||||
await Connection.DeleteAsync<MailCategoryAssignment>(assignment.Id).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var categoryId in assignmentsToAdd)
|
|
||||||
{
|
|
||||||
await Connection.InsertAsync(new MailCategoryAssignment
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
MailCategoryId = categoryId,
|
|
||||||
MailCopyUniqueId = mailCopyUniqueId
|
|
||||||
}, typeof(MailCategoryAssignment)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(accountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AssignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds)
|
|
||||||
{
|
|
||||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
|
||||||
if (uniqueIds.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
|
||||||
if (category == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
|
||||||
var query = $"SELECT * FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ? AND {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders})";
|
|
||||||
var existingAssignments = await Connection.QueryAsync<MailCategoryAssignment>(
|
|
||||||
query,
|
|
||||||
[categoryId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
|
|
||||||
var existingUniqueIds = existingAssignments.Select(a => a.MailCopyUniqueId).ToHashSet();
|
|
||||||
|
|
||||||
foreach (var uniqueId in uniqueIds.Where(a => !existingUniqueIds.Contains(a)))
|
|
||||||
{
|
|
||||||
await Connection.InsertAsync(new MailCategoryAssignment
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
MailCategoryId = categoryId,
|
|
||||||
MailCopyUniqueId = uniqueId
|
|
||||||
}, typeof(MailCategoryAssignment)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(category.MailAccountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnassignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds)
|
|
||||||
{
|
|
||||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
|
||||||
if (uniqueIds.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
|
|
||||||
if (category == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
|
||||||
await Connection.ExecuteAsync(
|
|
||||||
$"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ? AND {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders})",
|
|
||||||
[categoryId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(category.MailAccountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<MailCategory>> GetCategoriesForMailAsync(Guid accountId, IEnumerable<Guid> mailCopyUniqueIds)
|
|
||||||
{
|
|
||||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
|
||||||
if (uniqueIds.Count == 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
|
||||||
var sql = $"SELECT DISTINCT MailCategory.* FROM {nameof(MailCategory)} " +
|
|
||||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCategoryId)} " +
|
|
||||||
$"WHERE {nameof(MailCategory)}.{nameof(MailCategory.MailAccountId)} = ? AND {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders}) " +
|
|
||||||
$"ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE";
|
|
||||||
|
|
||||||
return await Connection.QueryAsync<MailCategory>(
|
|
||||||
sql,
|
|
||||||
[accountId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Guid>> GetAssignedCategoryIdsForAllAsync(IEnumerable<Guid> mailCopyUniqueIds)
|
|
||||||
{
|
|
||||||
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
|
|
||||||
if (uniqueIds.Count == 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
|
||||||
var sql = $"SELECT {nameof(MailCategoryAssignment.MailCategoryId)} " +
|
|
||||||
$"FROM {nameof(MailCategoryAssignment)} " +
|
|
||||||
$"WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders}) " +
|
|
||||||
$"GROUP BY {nameof(MailCategoryAssignment.MailCategoryId)} " +
|
|
||||||
$"HAVING COUNT(DISTINCT {nameof(MailCategoryAssignment.MailCopyUniqueId)}) = ?";
|
|
||||||
|
|
||||||
return await Connection.QueryScalarsAsync<Guid>(
|
|
||||||
sql,
|
|
||||||
[.. uniqueIds.Cast<object>(), uniqueIds.Count]).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<string>> GetCategoryNamesForMailAsync(Guid mailCopyUniqueId)
|
|
||||||
{
|
|
||||||
var sql = $"SELECT {nameof(MailCategory.Name)} " +
|
|
||||||
$"FROM {nameof(MailCategory)} " +
|
|
||||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment.MailCategoryId)} " +
|
|
||||||
$"WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} = ? " +
|
|
||||||
$"ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE";
|
|
||||||
|
|
||||||
return await Connection.QueryScalarsAsync<string>(sql, mailCopyUniqueId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<MailCopy>> GetMailCopiesForCategoryAsync(Guid categoryId)
|
|
||||||
{
|
|
||||||
var sql = $"SELECT {nameof(MailCopy)}.* " +
|
|
||||||
$"FROM {nameof(MailCopy)} " +
|
|
||||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCopy)}.{nameof(MailCopy.UniqueId)} = {nameof(MailCategoryAssignment.MailCopyUniqueId)} " +
|
|
||||||
$"WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ?";
|
|
||||||
|
|
||||||
return await Connection.QueryAsync<MailCopy>(sql, categoryId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<UnreadCategoryCountResult>> GetUnreadCategoryCountResultsAsync(IEnumerable<Guid> accountIds)
|
|
||||||
{
|
|
||||||
var accountIdList = accountIds?.Distinct().ToList() ?? [];
|
|
||||||
if (accountIdList.Count == 0)
|
|
||||||
return Task.FromResult(new List<UnreadCategoryCountResult>());
|
|
||||||
|
|
||||||
var placeholders = string.Join(",", accountIdList.Select(_ => "?"));
|
|
||||||
var sql =
|
|
||||||
$"SELECT MailCategory.{nameof(MailCategory.Id)} as {nameof(UnreadCategoryCountResult.CategoryId)}, " +
|
|
||||||
$"MailCategory.{nameof(MailCategory.MailAccountId)} as {nameof(UnreadCategoryCountResult.AccountId)}, " +
|
|
||||||
$"COUNT(DISTINCT MailCopy.{nameof(MailCopy.UniqueId)}) as {nameof(UnreadCategoryCountResult.UnreadItemCount)} " +
|
|
||||||
$"FROM {nameof(MailCategory)} " +
|
|
||||||
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCategoryId)} " +
|
|
||||||
$"INNER JOIN {nameof(MailCopy)} ON {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCopyUniqueId)} = {nameof(MailCopy)}.{nameof(MailCopy.UniqueId)} " +
|
|
||||||
$"WHERE MailCategory.{nameof(MailCategory.MailAccountId)} IN ({placeholders}) AND MailCopy.{nameof(MailCopy.IsRead)} = 0 " +
|
|
||||||
$"GROUP BY MailCategory.{nameof(MailCategory.Id)}";
|
|
||||||
|
|
||||||
return Connection.QueryAsync<UnreadCategoryCountResult>(sql, accountIdList.Cast<object>().ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NotifyCategoryStructureChanged(Guid accountId)
|
|
||||||
{
|
|
||||||
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested(false));
|
|
||||||
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(accountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeCategoryName(string name)
|
|
||||||
=> name?.Trim() ?? string.Empty;
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
private readonly IMimeFileService _mimeFileService;
|
private readonly IMimeFileService _mimeFileService;
|
||||||
private readonly IPreferencesService _preferencesService;
|
private readonly IPreferencesService _preferencesService;
|
||||||
private readonly ISentMailReceiptService _sentMailReceiptService;
|
private readonly ISentMailReceiptService _sentMailReceiptService;
|
||||||
private readonly IMailCategoryService _mailCategoryService;
|
|
||||||
|
|
||||||
private readonly ILogger _logger = Log.ForContext<MailService>();
|
private readonly ILogger _logger = Log.ForContext<MailService>();
|
||||||
|
|
||||||
@@ -43,8 +42,7 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
ISignatureService signatureService,
|
ISignatureService signatureService,
|
||||||
IMimeFileService mimeFileService,
|
IMimeFileService mimeFileService,
|
||||||
IPreferencesService preferencesService,
|
IPreferencesService preferencesService,
|
||||||
ISentMailReceiptService sentMailReceiptService,
|
ISentMailReceiptService sentMailReceiptService) : base(databaseService)
|
||||||
IMailCategoryService mailCategoryService) : base(databaseService)
|
|
||||||
{
|
{
|
||||||
_folderService = folderService;
|
_folderService = folderService;
|
||||||
_contactService = contactService;
|
_contactService = contactService;
|
||||||
@@ -53,7 +51,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
_mimeFileService = mimeFileService;
|
_mimeFileService = mimeFileService;
|
||||||
_preferencesService = preferencesService;
|
_preferencesService = preferencesService;
|
||||||
_sentMailReceiptService = sentMailReceiptService;
|
_sentMailReceiptService = sentMailReceiptService;
|
||||||
_mailCategoryService = mailCategoryService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
||||||
@@ -174,9 +171,7 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
private static (string Query, object[] Parameters) BuildMailFetchQuery(MailListInitializationOptions options)
|
private static (string Query, object[] Parameters) BuildMailFetchQuery(MailListInitializationOptions options)
|
||||||
{
|
{
|
||||||
var sql = new StringBuilder();
|
var sql = new StringBuilder();
|
||||||
sql.Append(options.IsCategoryView
|
sql.Append("SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id");
|
||||||
? "SELECT DISTINCT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id INNER JOIN MailCategoryAssignment ON MailCopy.UniqueId = MailCategoryAssignment.MailCopyUniqueId"
|
|
||||||
: "SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id");
|
|
||||||
|
|
||||||
var whereClauses = new List<string>();
|
var whereClauses = new List<string>();
|
||||||
var parameters = new List<object>();
|
var parameters = new List<object>();
|
||||||
@@ -186,13 +181,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
whereClauses.Add($"MailCopy.FolderId IN ({folderPlaceholders})");
|
whereClauses.Add($"MailCopy.FolderId IN ({folderPlaceholders})");
|
||||||
parameters.AddRange(options.Folders.Select(f => (object)f.Id));
|
parameters.AddRange(options.Folders.Select(f => (object)f.Id));
|
||||||
|
|
||||||
if (options.IsCategoryView)
|
|
||||||
{
|
|
||||||
var categoryPlaceholders = string.Join(",", options.CategoryIds.Select(_ => "?"));
|
|
||||||
whereClauses.Add($"MailCategoryAssignment.MailCategoryId IN ({categoryPlaceholders})");
|
|
||||||
parameters.AddRange(options.CategoryIds.Select(a => (object)a));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter type
|
// Filter type
|
||||||
switch (options.FilterType)
|
switch (options.FilterType)
|
||||||
{
|
{
|
||||||
@@ -350,7 +338,7 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
{
|
{
|
||||||
List<MailCopy> mails;
|
List<MailCopy> mails;
|
||||||
|
|
||||||
if (options.PreFetchMailCopies != null && !options.IsCategoryView)
|
if (options.PreFetchMailCopies != null)
|
||||||
{
|
{
|
||||||
mails = ApplyOptionsToPreFetchedMails(options);
|
mails = ApplyOptionsToPreFetchedMails(options);
|
||||||
}
|
}
|
||||||
@@ -410,7 +398,7 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
mails.RemoveAll(m => m.AssignedAccount == null || m.AssignedFolder == null);
|
mails.RemoveAll(m => m.AssignedAccount == null || m.AssignedFolder == null);
|
||||||
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
|
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!options.CreateThreads || mails.Count == 0 || options.IsCategoryView)
|
if (!options.CreateThreads || mails.Count == 0)
|
||||||
return [.. mails];
|
return [.. mails];
|
||||||
|
|
||||||
// 6. Expand threads: one batch query for all sibling mails across all threads.
|
// 6. Expand threads: one batch query for all sibling mails across all threads.
|
||||||
@@ -739,7 +727,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
_logger.Debug("Deleting mail {Id} from folder {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName);
|
_logger.Debug("Deleting mail {Id} from folder {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName);
|
||||||
|
|
||||||
await Connection.DeleteAsync<MailCopy>(mailCopy.UniqueId).ConfigureAwait(false);
|
await Connection.DeleteAsync<MailCopy>(mailCopy.UniqueId).ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("DELETE FROM MailCategoryAssignment WHERE MailCopyUniqueId = ?", mailCopy.UniqueId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// If there are no more copies exists of the same mail, delete the MIME file as well.
|
// If there are no more copies exists of the same mail, delete the MIME file as well.
|
||||||
var isMailExists = await IsMailExistsAsync(mailCopy.Id).ConfigureAwait(false);
|
var isMailExists = await IsMailExistsAsync(mailCopy.Id).ConfigureAwait(false);
|
||||||
@@ -978,7 +965,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
mailCopy.UniqueId = existingCopyItem.UniqueId;
|
mailCopy.UniqueId = existingCopyItem.UniqueId;
|
||||||
|
|
||||||
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
|
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
|
||||||
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
|
|
||||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -995,7 +981,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await InsertMailAsync(mailCopy).ConfigureAwait(false);
|
await InsertMailAsync(mailCopy).ConfigureAwait(false);
|
||||||
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
|
|
||||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1032,11 +1017,6 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
|
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ReplaceMailCategoriesForPackageAsync(Guid accountId, MailCopy mailCopy, NewMailItemPackage package)
|
|
||||||
=> package?.CategoryNames == null
|
|
||||||
? Task.CompletedTask
|
|
||||||
: _mailCategoryService.ReplaceMailAssignmentsAsync(accountId, mailCopy.UniqueId, package.CategoryNames);
|
|
||||||
|
|
||||||
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions, MailAccountAlias selectedAlias)
|
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions, MailAccountAlias selectedAlias)
|
||||||
{
|
{
|
||||||
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
|
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ public static class ServicesContainerSetup
|
|||||||
|
|
||||||
services.AddTransient<ICalendarService, CalendarService>();
|
services.AddTransient<ICalendarService, CalendarService>();
|
||||||
services.AddTransient<IMailService, MailService>();
|
services.AddTransient<IMailService, MailService>();
|
||||||
services.AddTransient<IMailCategoryService, MailCategoryService>();
|
|
||||||
services.AddTransient<ISentMailReceiptService, SentMailReceiptService>();
|
services.AddTransient<ISentMailReceiptService, SentMailReceiptService>();
|
||||||
services.AddTransient<IFolderService, FolderService>();
|
services.AddTransient<IFolderService, FolderService>();
|
||||||
services.AddTransient<IAccountService, AccountService>();
|
services.AddTransient<IAccountService, AccountService>();
|
||||||
|
|||||||
+1
-1
@@ -14,13 +14,13 @@
|
|||||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||||
<packageSource key="nuget">
|
<packageSource key="nuget">
|
||||||
<package pattern="*" />
|
<package pattern="*" />
|
||||||
<package pattern="CommunityToolkit.WinUI.Extensions" />
|
|
||||||
<!-- <package pattern="CommunityToolkit.Common" />
|
<!-- <package pattern="CommunityToolkit.Common" />
|
||||||
<package pattern="Microsoft.*" />
|
<package pattern="Microsoft.*" />
|
||||||
<package pattern="Newtonsoft.Json" /> -->
|
<package pattern="Newtonsoft.Json" /> -->
|
||||||
</packageSource>
|
</packageSource>
|
||||||
<packageSource key="labsFeed">
|
<packageSource key="labsFeed">
|
||||||
<package pattern="CommunityToolkit.Labs.*" />
|
<package pattern="CommunityToolkit.Labs.*" />
|
||||||
|
<package pattern="CommunityToolkit.WinUI.Extensions" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
</packageSourceMapping>
|
</packageSourceMapping>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
Reference in New Issue
Block a user