Add initial mail sync range selection

This commit is contained in:
Burak Kaan Köse
2026-04-14 00:03:48 +02:00
parent 2e36772a4c
commit c622858d2d
21 changed files with 378 additions and 73 deletions
@@ -112,6 +112,16 @@ public class MailAccount
/// </summary>
public DateTime? LastFolderStructureSyncDate { get; set; }
/// <summary>
/// Gets or sets when the account was created in Wino.
/// </summary>
public DateTime? CreatedAt { get; set; }
/// <summary>
/// Gets or sets the timespan used for the account's initial mail synchronization.
/// </summary>
public InitialSynchronizationRange InitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
/// <summary>
/// Gets whether the account can perform ProfileInformation sync type.
/// </summary>
@@ -0,0 +1,10 @@
namespace Wino.Core.Domain.Enums;
public enum InitialSynchronizationRange
{
SixMonths = 0,
ThreeMonths = 1,
NineMonths = 2,
OneYear = 3,
Everything = 4
}
@@ -0,0 +1,23 @@
using System;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Extensions;
public static class InitialSynchronizationRangeExtensions
{
public static DateTime? ToCutoffDateUtc(this InitialSynchronizationRange range, DateTime utcNow)
{
var normalizedUtcNow = utcNow.Kind == DateTimeKind.Utc
? utcNow
: utcNow.ToUniversalTime();
return range switch
{
InitialSynchronizationRange.ThreeMonths => normalizedUtcNow.AddMonths(-3),
InitialSynchronizationRange.SixMonths => normalizedUtcNow.AddMonths(-6),
InitialSynchronizationRange.NineMonths => normalizedUtcNow.AddMonths(-9),
InitialSynchronizationRange.OneYear => normalizedUtcNow.AddYears(-1),
_ => null
};
}
}
@@ -1,5 +1,10 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Accounts;
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, SpecialImapProviderDetails SpecialImapProviderDetails, string AccountColorHex);
public record AccountCreationDialogResult(
MailProviderType ProviderType,
string AccountName,
SpecialImapProviderDetails SpecialImapProviderDetails,
string AccountColorHex,
InitialSynchronizationRange InitialSynchronizationRange);
@@ -0,0 +1,17 @@
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Accounts;
public sealed class InitialSynchronizationRangeOption
{
public InitialSynchronizationRange Range { get; }
public string DisplayText { get; }
public bool IsEverything => Range == InitialSynchronizationRange.Everything;
public InitialSynchronizationRangeOption(InitialSynchronizationRange range, string displayText)
{
Range = range;
DisplayText = displayText;
}
}
@@ -23,6 +23,14 @@
"AccountCreationDialog_Initializing": "initializing",
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
"AccountCreationDialog_SigninIn": "Account information is being saved.",
"AccountCreation_InitialSynchronization_Title": "Mail synchronization range",
"AccountCreation_InitialSynchronization_Description": "Choose how far back Wino should download your mail during the first synchronization.",
"AccountCreation_InitialSynchronization_3Months": "3 Months",
"AccountCreation_InitialSynchronization_6Months": "6 Months",
"AccountCreation_InitialSynchronization_9Months": "9 Months",
"AccountCreation_InitialSynchronization_Year": "Year",
"AccountCreation_InitialSynchronization_Everything": "Everything",
"AccountCreation_InitialSynchronization_EverythingWarning": "This will synchronize all your mails to your computer. Extensive use of disk storage is needed. This is not recommended. For optimal performance use smaller synchronization timespan and use online search to access your mails.",
"Purchased": "Purchased",
"AccountEditDialog_Message": "Account Name",
"AccountEditDialog_Title": "Edit Account",
@@ -37,6 +45,8 @@
"AccountDetailsPage_TabMail": "Mail",
"AccountDetailsPage_TabCalendar": "Calendar",
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings",
"AccountDetailsPage_InitialSynchronization_Title": "Initial synchronization",
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronized your mails until {0} going back.",
"AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
+11 -14
View File
@@ -81,9 +81,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
public override uint BatchModificationSize => 1000;
/// <summary>
/// Maximum messages to fetch per folder during initial sync (1500).
/// All messages are downloaded with METADATA ONLY - no raw MIME content.
/// Uses Gmail API's Metadata format which includes headers, labels, and snippet but NOT full message body.
/// Legacy page size hint kept for compatibility with shared synchronizer contracts.
/// Gmail initial sync now downloads all messages inside the selected cutoff window.
/// </summary>
public override uint InitialMessageDownloadCountPerFolder => 1500;
@@ -304,13 +303,18 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
/// <summary>
/// Performs initial synchronization by downloading messages per-folder.
/// Each folder gets up to 1500 messages, but we track already downloaded message IDs globally
/// to avoid downloading the same message multiple times (Gmail messages can have multiple labels).
/// Messages are filtered by the account's configured initial synchronization cutoff date when present,
/// and duplicates are avoided globally because Gmail messages can have multiple labels.
/// </summary>
private async Task<List<string>> PerformInitialSyncAsync(CancellationToken cancellationToken)
{
// Track all downloaded message IDs globally to avoid duplicate downloads
var downloadedMessageIds = new HashSet<string>();
var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
var queryText = initialSynchronizationCutoffDateUtc.HasValue
? $"after:{initialSynchronizationCutoffDateUtc.Value.ToUniversalTime():yyyy/MM/dd}"
: null;
_logger.Information("Performing initial sync for {Name} - downloading messages per folder", Account.Name);
@@ -337,7 +341,6 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
var folderDownloaded = 0;
string pageToken = null;
var remainingToDownload = (int)InitialMessageDownloadCountPerFolder;
do
{
@@ -345,8 +348,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
var request = _gmailService.Users.Messages.List("me");
request.LabelIds = new Google.Apis.Util.Repeatable<string>(new[] { folder.RemoteFolderId });
request.MaxResults = Math.Min(remainingToDownload, 500); // API max is 500
request.MaxResults = 500; // API max is 500
request.PageToken = pageToken;
request.Q = queryText;
var response = await request.ExecuteAsync(cancellationToken);
@@ -373,19 +377,12 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
totalMessagesDownloaded += newMessageIds.Count;
}
// Count all messages (including duplicates) toward the folder limit
remainingToDownload -= response.Messages.Count;
_logger.Debug("Folder {FolderName}: Downloaded {New} new messages ({Total} total in folder)",
folder.FolderName, newMessageIds.Count, folderDownloaded);
}
pageToken = response.NextPageToken;
// Stop if we've processed enough messages for this folder or no more pages
if (remainingToDownload <= 0 || string.IsNullOrEmpty(pageToken))
break;
} while (!string.IsNullOrEmpty(pageToken));
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
@@ -9,6 +9,7 @@ using MailKit.Search;
using MoreLinq;
using Serilog;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
@@ -252,9 +253,20 @@ public class UnifiedImapSynchronizer
.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken)
.ConfigureAwait(false);
var changedUids = await remoteFolder
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
.ConfigureAwait(false);
IList<UniqueId> changedUids;
if (folder.HighestModeSeq == 0)
{
changedUids = await remoteFolder
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
.ConfigureAwait(false);
}
else
{
changedUids = await remoteFolder
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
.ConfigureAwait(false);
}
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
@@ -308,25 +320,26 @@ public class UnifiedImapSynchronizer
{
IList<UniqueId> changedUids;
if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
if (isInitialSync)
{
changedUids = await remoteFolder
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
.ConfigureAwait(false);
}
else
{
changedUids = await remoteFolder
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
.ConfigureAwait(false);
}
if (isInitialSync)
{
changedUids = changedUids
.OrderByDescending(a => a.Id)
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
.ToList();
if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
{
changedUids = await remoteFolder
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
.ConfigureAwait(false);
}
else
{
changedUids = await remoteFolder
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
.ConfigureAwait(false);
}
}
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
@@ -367,15 +380,12 @@ public class UnifiedImapSynchronizer
if (folder.HighestKnownUid == 0)
{
var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);
var initialUids = remoteUids
.OrderByDescending(a => a.Id)
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
.ToList();
var initialUids = await remoteFolder
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
.ConfigureAwait(false);
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, initialUids, synchronizer, cancellationToken).ConfigureAwait(false);
UpdateHighestKnownUid(folder, remoteFolder, remoteUids.Select(a => a.Id));
UpdateHighestKnownUid(folder, remoteFolder, initialUids.Select(a => a.Id));
}
else
{
@@ -410,6 +420,22 @@ public class UnifiedImapSynchronizer
#region Shared Helpers
private static SearchQuery BuildInitialSyncQuery(IImapSynchronizer synchronizer)
{
if (synchronizer is IBaseSynchronizer { Account: { } account })
{
var referenceDateUtc = account.CreatedAt ?? DateTime.UtcNow;
var cutoffDateUtc = account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
if (cutoffDateUtc.HasValue)
{
return SearchQuery.DeliveredAfter(cutoffDateUtc.Value.ToUniversalTime().Date);
}
}
return SearchQuery.All;
}
private async Task EnsureUidValidityStateAsync(MailItemFolder folder, IMailFolder remoteFolder)
{
if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity)
+23 -13
View File
@@ -55,14 +55,14 @@ public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
///
/// SYNCHRONIZATION STRATEGY:
/// - Uses delta API for both initial and incremental sync
/// - Initial sync: Downloads last 30 days of emails with metadata only
/// - Initial sync: Downloads messages using the account's configured cutoff date with metadata only
/// - Incremental sync: Uses delta token to get only changes since last sync
/// - Messages are downloaded with metadata only (no MIME content during sync)
/// - MIME files are downloaded on-demand when user explicitly reads a message
///
/// Key implementation details:
/// - SynchronizeFolderAsync: Main entry point for per-folder synchronization
/// - DownloadMailsForInitialSyncAsync: Downloads last 30 days using delta API with filter
/// - DownloadMailsForInitialSyncAsync: Downloads messages using delta API with an optional cutoff filter
/// - ProcessDeltaChangesAsync: Processes incremental changes using delta token
/// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API
/// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata
@@ -343,9 +343,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
// Check if we have a delta token
if (string.IsNullOrEmpty(folder.DeltaToken))
{
_logger.Debug("No delta token for folder {FolderName}. Starting initial sync (last 30 days).", folder.FolderName);
_logger.Debug("No delta token for folder {FolderName}. Starting initial sync.", folder.FolderName);
// Download mails for initial sync (last 30 days)
// Download mails for initial sync using the account's configured cutoff date.
await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
}
else
@@ -367,27 +367,37 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
}
/// <summary>
/// Downloads mails for initial synchronization using Delta API with 30-day filter.
/// Downloads metadata only (no MIME content) for messages received in the last 30 days.
/// Downloads mails for initial synchronization using Delta API with the account's configured cutoff date.
/// Downloads metadata only (no MIME content) for messages received after that date.
/// </summary>
private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken)
{
_logger.Debug("Starting initial mail download for folder {FolderName} (last 6 months)", folder.FolderName);
_logger.Debug("Starting initial mail download for folder {FolderName}", folder.FolderName);
try
{
// Calculate date 6 months ago
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6);
var filterDate = sixMonthsAgo.ToString("yyyy-MM-ddTHH:mm:ssZ");
var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
var filterDate = initialSynchronizationCutoffDateUtc?.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
_logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName);
if (filterDate != null)
{
_logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName);
}
else
{
_logger.Information("Downloading all available messages for folder {FolderName}", folder.FolderName);
}
// Use Delta API with receivedDateTime filter for last 6 months
var messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
{
config.QueryParameters.Select = outlookMessageSelectParameters;
config.QueryParameters.Orderby = ["receivedDateTime desc"];
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
if (filterDate != null)
{
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
}
}, cancellationToken).ConfigureAwait(false);
var totalProcessed = 0;
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -12,6 +13,7 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders;
@@ -101,6 +103,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
public string Address => Account?.Address ?? string.Empty;
public bool IsInitialSynchronizationSummaryVisible => Account?.CreatedAt.HasValue == true && Account.InitialSynchronizationRange != InitialSynchronizationRange.Everything;
public string InitialSynchronizationSummary => Account?.CreatedAt is not DateTime createdAtUtc
? string.Empty
: Account.InitialSynchronizationRange.ToCutoffDateUtc(createdAtUtc) is not DateTime cutoffDateUtc
? string.Empty
: string.Format(
Translator.AccountDetailsPage_InitialSynchronization_Description,
cutoffDateUtc.ToLocalTime().ToString("D", CultureInfo.CurrentUICulture));
public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
[
@@ -363,6 +373,8 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
OnPropertyChanged(nameof(ProviderIconPath));
OnPropertyChanged(nameof(Address));
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
OnPropertyChanged(nameof(InitialSynchronizationSummary));
}
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
@@ -170,6 +170,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
try
{
CustomServerInformation customServerInformation = null;
var accountCreatedAt = DateTime.UtcNow;
// Build account in memory
_createdAccount = new MailAccount
@@ -179,6 +180,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
Name = WizardContext.AccountName,
SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
AccountColorHex = WizardContext.AccountColorHex,
CreatedAt = accountCreatedAt,
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
IsCalendarAccessGranted = true
};
@@ -18,6 +18,9 @@ public partial class WelcomeWizardContext : ObservableObject
[ObservableProperty]
public partial string AccountColorHex { get; set; }
[ObservableProperty]
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
// Special IMAP fields (iCloud/Yahoo)
[ObservableProperty]
public partial string DisplayName { get; set; }
@@ -62,7 +65,8 @@ public partial class WelcomeWizardContext : ObservableObject
SelectedProvider.Type,
AccountName,
BuildSpecialImapProviderDetails(),
AccountColorHex);
AccountColorHex,
SelectedInitialSynchronizationRange);
}
public void Reset()
@@ -70,6 +74,7 @@ public partial class WelcomeWizardContext : ObservableObject
SelectedProvider = null;
AccountName = null;
AccountColorHex = null;
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
DisplayName = null;
EmailAddress = null;
AppSpecificPassword = null;
@@ -997,7 +997,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SpecialImapProvider = _editingSpecialImapProvider,
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
},
new AccountCreationDialogResult(MailProviderType.IMAP4, DisplayName.Trim(), providerDetails, string.Empty));
new AccountCreationDialogResult(
MailProviderType.IMAP4,
DisplayName.Trim(),
providerDetails,
string.Empty,
_wizardContext.SelectedInitialSynchronizationRange));
if (serverInformation == null)
return false;
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels.Data;
using Wino.Mail.ViewModels.Data;
@@ -22,13 +23,26 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
public List<IProviderDetail> Providers { get; private set; } = [];
public List<AppColorViewModel> AvailableColors { get; private set; } = [];
public List<InitialSynchronizationRangeOption> InitialSynchronizationRanges { get; } =
[
new(InitialSynchronizationRange.ThreeMonths, Translator.AccountCreation_InitialSynchronization_3Months),
new(InitialSynchronizationRange.SixMonths, Translator.AccountCreation_InitialSynchronization_6Months),
new(InitialSynchronizationRange.NineMonths, Translator.AccountCreation_InitialSynchronization_9Months),
new(InitialSynchronizationRange.OneYear, Translator.AccountCreation_InitialSynchronization_Year),
new(InitialSynchronizationRange.Everything, Translator.AccountCreation_InitialSynchronization_Everything)
];
[ObservableProperty]
public partial IProviderDetail SelectedProvider { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsColorSelected))]
public partial AppColorViewModel SelectedColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
[ObservableProperty]
public partial string AccountName { get; set; }
@@ -36,6 +50,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
public partial bool CanProceed { get; set; }
public bool IsColorSelected => SelectedColor != null;
public bool IsInitialSynchronizationWarningVisible => SelectedInitialSynchronizationRange?.IsEverything == true;
public ProviderSelectionPageViewModel(
IProviderService providerService,
@@ -45,6 +60,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
_providerService = providerService;
_themeService = themeService;
WizardContext = wizardContext;
SelectedInitialSynchronizationRange = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -56,6 +72,10 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
.Select(hex => new AppColorViewModel(hex))
.ToList();
SelectedInitialSynchronizationRange = InitialSynchronizationRanges
.FirstOrDefault(option => option.Range == WizardContext.SelectedInitialSynchronizationRange)
?? InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
// Restore from wizard context if navigating back
if (WizardContext.SelectedProvider != null)
{
@@ -71,9 +91,12 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
Validate();
}
partial void OnSelectedProviderChanged(IProviderDetail value) => Validate();
partial void OnSelectedProviderChanged(IProviderDetail value)
{
Validate();
}
partial void OnAccountNameChanged(string value) => Validate();
partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected));
[RelayCommand]
private void ClearColor() => SelectedColor = null;
@@ -92,6 +115,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
WizardContext.SelectedProvider = SelectedProvider;
WizardContext.AccountName = AccountName?.Trim();
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
if (WizardContext.IsGenericImap)
{
@@ -55,6 +55,7 @@
<Grid MinWidth="400" RowSpacing="12">
<Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
@@ -96,6 +97,48 @@
</Grid>
<Border
x:Name="InitialSynchronizationPanel"
Grid.Row="1"
Margin="0,12,0,0"
Padding="12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Visibility="Collapsed">
<StackPanel Spacing="10">
<StackPanel Spacing="2">
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Title}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Description}"
TextWrapping="WrapWholeWords" />
</StackPanel>
<ComboBox
x:Name="InitialSynchronizationComboBox"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind InitialSynchronizationRanges, Mode=OneWay}"
SelectionChanged="InitialSynchronizationSelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="accounts:InitialSynchronizationRangeOption">
<TextBlock Text="{x:Bind DisplayText}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<muxc:InfoBar
x:Name="InitialSynchronizationWarningBar"
IsOpen="True"
Message="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_EverythingWarning}"
Severity="Warning"
Title="{x:Bind domain:Translator.GeneralTitle_Warning}"
Visibility="Collapsed" />
</StackPanel>
</Border>
<ListView
Grid.Row="2"
@@ -15,7 +15,7 @@ namespace Wino.Mail.WinUI.Dialogs;
public sealed partial class NewAccountDialog : ContentDialog
{
private readonly Dictionary<SpecialImapProvider, string> helpingLinks = new Dictionary<SpecialImapProvider, string>()
private readonly Dictionary<SpecialImapProvider, string> helpingLinks = new()
{
{ SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
@@ -27,7 +27,6 @@ public sealed partial class NewAccountDialog : ContentDialog
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(AppColorViewModel), typeof(NewAccountDialog), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedColorChanged)));
public static readonly DependencyProperty SelectedCalendarModeIndexProperty = DependencyProperty.Register(nameof(SelectedCalendarModeIndex), typeof(int), typeof(NewAccountDialog), new PropertyMetadata(0));
public AppColorViewModel? SelectedColor
{
get { return (AppColorViewModel?)GetValue(SelectedColorProperty); }
@@ -49,7 +48,6 @@ public sealed partial class NewAccountDialog : ContentDialog
set { SetValue(SelectedMailProviderProperty, value); }
}
public bool IsProviderSelectionVisible
{
get { return (bool)GetValue(IsProviderSelectionVisibleProperty); }
@@ -63,10 +61,16 @@ public sealed partial class NewAccountDialog : ContentDialog
}
// List of available mail providers for now.
public List<IProviderDetail> Providers { get; set; } = [];
public List<AppColorViewModel> AvailableColors { get; set; } = [];
public List<InitialSynchronizationRangeOption> InitialSynchronizationRanges { get; } =
[
new(InitialSynchronizationRange.ThreeMonths, Translator.AccountCreation_InitialSynchronization_3Months),
new(InitialSynchronizationRange.SixMonths, Translator.AccountCreation_InitialSynchronization_6Months),
new(InitialSynchronizationRange.NineMonths, Translator.AccountCreation_InitialSynchronization_9Months),
new(InitialSynchronizationRange.OneYear, Translator.AccountCreation_InitialSynchronization_Year),
new(InitialSynchronizationRange.Everything, Translator.AccountCreation_InitialSynchronization_Everything)
];
public List<string> CalendarModeOptions { get; } =
[
Translator.ImapCalDavSettingsPage_CalendarModeCalDav,
@@ -74,7 +78,6 @@ public sealed partial class NewAccountDialog : ContentDialog
Translator.ImapCalDavSettingsPage_CalendarModeDisabled
];
public AccountCreationDialogResult? Result = null;
public NewAccountDialog()
@@ -85,6 +88,8 @@ public sealed partial class NewAccountDialog : ContentDialog
AvailableColors = themeService.Select(a => new AppColorViewModel(a)).ToList();
UpdateSelectedColor();
InitialSynchronizationComboBox.SelectedItem = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
UpdateInitialSynchronizationState();
}
private static void OnSelectedProviderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
@@ -105,6 +110,19 @@ public sealed partial class NewAccountDialog : ContentDialog
SelectedColorEllipse.Fill = SelectedColor == null ? null : XamlHelpers.GetSolidColorBrushFromHex(SelectedColor.Hex);
}
private void UpdateInitialSynchronizationState()
{
InitialSynchronizationPanel.Visibility = SelectedMailProvider == null ? Visibility.Collapsed : Visibility.Visible;
var selectedOption = InitialSynchronizationComboBox.SelectedItem as InitialSynchronizationRangeOption;
InitialSynchronizationWarningBar.Visibility = selectedOption?.IsEverything == true ? Visibility.Visible : Visibility.Collapsed;
}
private InitialSynchronizationRange GetInitialSynchronizationRange()
{
var selectedRange = (InitialSynchronizationComboBox.SelectedItem as InitialSynchronizationRangeOption)?.Range
?? InitialSynchronizationRange.SixMonths;
return selectedRange;
}
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
@@ -116,9 +134,11 @@ public sealed partial class NewAccountDialog : ContentDialog
if (SelectedMailProvider == null)
return;
var initialSynchronizationRange = GetInitialSynchronizationRange();
if (IsSpecialImapServerPartVisible)
{
// Special imap detail input.
// Special IMAP detail input.
var calendarSupportMode = SelectedCalendarModeIndex switch
{
1 => ImapCalendarSupportMode.LocalOnly,
@@ -132,7 +152,12 @@ public sealed partial class NewAccountDialog : ContentDialog
DisplayNameTextBox.Text.Trim(),
SelectedMailProvider.SpecialImapProvider,
calendarSupportMode);
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), details, SelectedColor?.Hex ?? string.Empty);
Result = new AccountCreationDialogResult(
SelectedMailProvider.Type,
AccountNameTextbox.Text.Trim(),
details,
SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange);
Hide();
return;
@@ -140,11 +165,11 @@ public sealed partial class NewAccountDialog : ContentDialog
Validate();
if (IsSecondaryButtonEnabled)
if (IsPrimaryButtonEnabled)
{
if (SelectedMailProvider.SpecialImapProvider != SpecialImapProvider.None)
{
// This step requires app-sepcific password login for some providers.
// This step requires app-specific password login for some providers.
args.Cancel = true;
IsProviderSelectionVisible = false;
@@ -154,7 +179,12 @@ public sealed partial class NewAccountDialog : ContentDialog
}
else
{
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), null, SelectedColor?.Hex ?? string.Empty);
Result = new AccountCreationDialogResult(
SelectedMailProvider.Type,
AccountNameTextbox.Text.Trim(),
null,
SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange);
Hide();
}
}
@@ -167,6 +197,7 @@ public sealed partial class NewAccountDialog : ContentDialog
{
ValidateCreateButton();
ValidateNames();
UpdateInitialSynchronizationState();
}
// Returns whether we can create account or not.
@@ -199,6 +230,9 @@ public sealed partial class NewAccountDialog : ContentDialog
private void ImapPasswordChanged(object sender, RoutedEventArgs e) => Validate();
private void InitialSynchronizationSelectionChanged(object sender, SelectionChangedEventArgs e)
=> UpdateInitialSynchronizationState();
private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e)
{
if (SelectedMailProvider == null ||
@@ -210,6 +210,13 @@
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<muxc:InfoBar
IsOpen="True"
Margin="0,0,0,8"
Message="{x:Bind ViewModel.InitialSynchronizationSummary, Mode=OneWay}"
Severity="Informational"
Title="{x:Bind domain:Translator.AccountDetailsPage_InitialSynchronization_Title}"
Visibility="{x:Bind ViewModel.IsInitialSynchronizationSummaryVisible, Mode=OneWay}" />
<controls:SettingsCard
Command="{x:Bind ViewModel.EditAliasesCommand}"
@@ -10,15 +10,12 @@
xmlns:helpers="using:Wino.Helpers"
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
<ScrollViewer
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalScrollBarVisibility="Auto">
<ScrollViewer HorizontalAlignment="Center" VerticalScrollBarVisibility="Auto">
<StackPanel
MaxWidth="480"
Margin="0,24,0,24"
Margin="0,12"
HorizontalAlignment="Stretch"
Spacing="20">
@@ -76,6 +73,50 @@
</Button>
</Grid>
<Border
MaxWidth="600"
Padding="12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8">
<StackPanel Spacing="10">
<StackPanel Spacing="2">
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Title}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Description}"
TextWrapping="WrapWholeWords" />
</StackPanel>
<ListView
HorizontalAlignment="Center"
HorizontalContentAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.InitialSynchronizationRanges, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedInitialSynchronizationRange, Mode=TwoWay}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="accounts:InitialSynchronizationRangeOption">
<TextBlock Text="{x:Bind DisplayText}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<muxc:InfoBar
Title="{x:Bind domain:Translator.GeneralTitle_Warning}"
Margin="0,2,0,0"
IsOpen="True"
Message="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_EverythingWarning}"
Severity="Warning"
Visibility="{x:Bind ViewModel.IsInitialSynchronizationWarningVisible, Mode=OneWay}" />
</StackPanel>
</Border>
<!-- Provider List -->
<ItemsView
HorizontalContentAlignment="Stretch"
+5
View File
@@ -590,6 +590,11 @@ public class AccountService : BaseDatabaseService, IAccountService
{
Guard.IsNotNull(account);
if (!account.CreatedAt.HasValue)
{
account.CreatedAt = DateTime.UtcNow;
}
var accountCount = await Connection.Table<MailAccount>().CountAsync();
// If there are no accounts before this one, set it as startup account.
+16
View File
@@ -79,6 +79,22 @@ public class DatabaseService : IDatabaseService
{
await EnsureKeyboardShortcutSchemaAsync().ConfigureAwait(false);
var accountColumns = await Connection.GetTableInfoAsync(nameof(MailAccount)).ConfigureAwait(false);
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.CreatedAt)))
{
await Connection
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.CreatedAt)} TEXT NULL")
.ConfigureAwait(false);
}
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.InitialSynchronizationRange)))
{
await Connection
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.InitialSynchronizationRange)} INTEGER NOT NULL DEFAULT {(int)InitialSynchronizationRange.SixMonths}")
.ConfigureAwait(false);
}
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
@@ -211,6 +211,8 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider,
AccountColorHex = mailbox.AccountColorHex?.Trim(),
Base64ProfilePictureData = string.Empty,
CreatedAt = DateTime.UtcNow,
InitialSynchronizationRange = InitialSynchronizationRange.SixMonths,
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
SynchronizationDeltaIdentifier = string.Empty,
CalendarSynchronizationDeltaIdentifier = string.Empty,