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> /// </summary>
public DateTime? LastFolderStructureSyncDate { get; set; } 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> /// <summary>
/// Gets whether the account can perform ProfileInformation sync type. /// Gets whether the account can perform ProfileInformation sync type.
/// </summary> /// </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; 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_Initializing": "initializing",
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.", "AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
"AccountCreationDialog_SigninIn": "Account information is being saved.", "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", "Purchased": "Purchased",
"AccountEditDialog_Message": "Account Name", "AccountEditDialog_Message": "Account Name",
"AccountEditDialog_Title": "Edit Account", "AccountEditDialog_Title": "Edit Account",
@@ -37,6 +45,8 @@
"AccountDetailsPage_TabMail": "Mail", "AccountDetailsPage_TabMail": "Mail",
"AccountDetailsPage_TabCalendar": "Calendar", "AccountDetailsPage_TabCalendar": "Calendar",
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings", "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", "AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", "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; public override uint BatchModificationSize => 1000;
/// <summary> /// <summary>
/// Maximum messages to fetch per folder during initial sync (1500). /// Legacy page size hint kept for compatibility with shared synchronizer contracts.
/// All messages are downloaded with METADATA ONLY - no raw MIME content. /// Gmail initial sync now downloads all messages inside the selected cutoff window.
/// Uses Gmail API's Metadata format which includes headers, labels, and snippet but NOT full message body.
/// </summary> /// </summary>
public override uint InitialMessageDownloadCountPerFolder => 1500; public override uint InitialMessageDownloadCountPerFolder => 1500;
@@ -304,13 +303,18 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
/// <summary> /// <summary>
/// Performs initial synchronization by downloading messages per-folder. /// Performs initial synchronization by downloading messages per-folder.
/// Each folder gets up to 1500 messages, but we track already downloaded message IDs globally /// Messages are filtered by the account's configured initial synchronization cutoff date when present,
/// to avoid downloading the same message multiple times (Gmail messages can have multiple labels). /// and duplicates are avoided globally because Gmail messages can have multiple labels.
/// </summary> /// </summary>
private async Task<List<string>> PerformInitialSyncAsync(CancellationToken cancellationToken) private async Task<List<string>> PerformInitialSyncAsync(CancellationToken cancellationToken)
{ {
// Track all downloaded message IDs globally to avoid duplicate downloads // Track all downloaded message IDs globally to avoid duplicate downloads
var downloadedMessageIds = new HashSet<string>(); 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); _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; var folderDownloaded = 0;
string pageToken = null; string pageToken = null;
var remainingToDownload = (int)InitialMessageDownloadCountPerFolder;
do do
{ {
@@ -345,8 +348,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
var request = _gmailService.Users.Messages.List("me"); var request = _gmailService.Users.Messages.List("me");
request.LabelIds = new Google.Apis.Util.Repeatable<string>(new[] { folder.RemoteFolderId }); 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.PageToken = pageToken;
request.Q = queryText;
var response = await request.ExecuteAsync(cancellationToken); var response = await request.ExecuteAsync(cancellationToken);
@@ -373,19 +377,12 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
totalMessagesDownloaded += newMessageIds.Count; 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)", _logger.Debug("Folder {FolderName}: Downloaded {New} new messages ({Total} total in folder)",
folder.FolderName, newMessageIds.Count, folderDownloaded); folder.FolderName, newMessageIds.Count, folderDownloaded);
} }
pageToken = response.NextPageToken; 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)); } while (!string.IsNullOrEmpty(pageToken));
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded); _logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
@@ -9,6 +9,7 @@ using MailKit.Search;
using MoreLinq; using MoreLinq;
using Serilog; using Serilog;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Extensions;
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;
@@ -252,9 +253,20 @@ public class UnifiedImapSynchronizer
.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken) .OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
var changedUids = await remoteFolder IList<UniqueId> changedUids;
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
.ConfigureAwait(false); 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); downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
@@ -308,25 +320,26 @@ public class UnifiedImapSynchronizer
{ {
IList<UniqueId> changedUids; IList<UniqueId> changedUids;
if (client.Capabilities.HasFlag(ImapCapabilities.Sort)) if (isInitialSync)
{ {
changedUids = await remoteFolder changedUids = await remoteFolder
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken) .SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
else else
{ {
changedUids = await remoteFolder if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken) {
.ConfigureAwait(false); changedUids = await remoteFolder
} .SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
.ConfigureAwait(false);
if (isInitialSync) }
{ else
changedUids = changedUids {
.OrderByDescending(a => a.Id) changedUids = await remoteFolder
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder) .SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
.ToList(); .ConfigureAwait(false);
}
} }
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, 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) if (folder.HighestKnownUid == 0)
{ {
var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); var initialUids = await remoteFolder
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
var initialUids = remoteUids .ConfigureAwait(false);
.OrderByDescending(a => a.Id)
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
.ToList();
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, initialUids, 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 else
{ {
@@ -410,6 +420,22 @@ public class UnifiedImapSynchronizer
#region Shared Helpers #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) private async Task EnsureUidValidityStateAsync(MailItemFolder folder, IMailFolder remoteFolder)
{ {
if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity) if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity)
+23 -13
View File
@@ -55,14 +55,14 @@ public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
/// ///
/// SYNCHRONIZATION STRATEGY: /// SYNCHRONIZATION STRATEGY:
/// - Uses delta API for both initial and incremental sync /// - 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 /// - Incremental sync: Uses delta token to get only changes since last sync
/// - Messages are downloaded with metadata only (no MIME content during sync) /// - Messages are downloaded with metadata only (no MIME content during sync)
/// - MIME files are downloaded on-demand when user explicitly reads a message /// - MIME files are downloaded on-demand when user explicitly reads a message
/// ///
/// Key implementation details: /// Key implementation details:
/// - SynchronizeFolderAsync: Main entry point for per-folder synchronization /// - 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 /// - ProcessDeltaChangesAsync: Processes incremental changes using delta token
/// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API /// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API
/// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata /// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata
@@ -343,9 +343,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
// Check if we have a delta token // Check if we have a delta token
if (string.IsNullOrEmpty(folder.DeltaToken)) 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); await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
} }
else else
@@ -367,27 +367,37 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
} }
/// <summary> /// <summary>
/// Downloads mails for initial synchronization using Delta API with 30-day filter. /// Downloads mails for initial synchronization using Delta API with the account's configured cutoff date.
/// Downloads metadata only (no MIME content) for messages received in the last 30 days. /// Downloads metadata only (no MIME content) for messages received after that date.
/// </summary> /// </summary>
private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken) 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 try
{ {
// Calculate date 6 months ago var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6); var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
var filterDate = sixMonthsAgo.ToString("yyyy-MM-ddTHH:mm:ssZ"); 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) => var messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
{ {
config.QueryParameters.Select = outlookMessageSelectParameters; config.QueryParameters.Select = outlookMessageSelectParameters;
config.QueryParameters.Orderby = ["receivedDateTime desc"]; config.QueryParameters.Orderby = ["receivedDateTime desc"];
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
if (filterDate != null)
{
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
}
}, cancellationToken).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false);
var totalProcessed = 0; var totalProcessed = 0;
@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -12,6 +13,7 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders; 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.SpecialImapProvider}.png"
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png"; : $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
public string Address => Account?.Address ?? string.Empty; 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; } = public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
[ [
@@ -363,6 +373,8 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount)); OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
OnPropertyChanged(nameof(ProviderIconPath)); OnPropertyChanged(nameof(ProviderIconPath));
OnPropertyChanged(nameof(Address)); OnPropertyChanged(nameof(Address));
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
OnPropertyChanged(nameof(InitialSynchronizationSummary));
} }
protected override async void OnPropertyChanged(PropertyChangedEventArgs e) protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
@@ -170,6 +170,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
try try
{ {
CustomServerInformation customServerInformation = null; CustomServerInformation customServerInformation = null;
var accountCreatedAt = DateTime.UtcNow;
// Build account in memory // Build account in memory
_createdAccount = new MailAccount _createdAccount = new MailAccount
@@ -179,6 +180,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
Name = WizardContext.AccountName, Name = WizardContext.AccountName,
SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider, SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
AccountColorHex = WizardContext.AccountColorHex, AccountColorHex = WizardContext.AccountColorHex,
CreatedAt = accountCreatedAt,
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
IsCalendarAccessGranted = true IsCalendarAccessGranted = true
}; };
@@ -18,6 +18,9 @@ public partial class WelcomeWizardContext : ObservableObject
[ObservableProperty] [ObservableProperty]
public partial string AccountColorHex { get; set; } public partial string AccountColorHex { get; set; }
[ObservableProperty]
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
// Special IMAP fields (iCloud/Yahoo) // Special IMAP fields (iCloud/Yahoo)
[ObservableProperty] [ObservableProperty]
public partial string DisplayName { get; set; } public partial string DisplayName { get; set; }
@@ -62,7 +65,8 @@ public partial class WelcomeWizardContext : ObservableObject
SelectedProvider.Type, SelectedProvider.Type,
AccountName, AccountName,
BuildSpecialImapProviderDetails(), BuildSpecialImapProviderDetails(),
AccountColorHex); AccountColorHex,
SelectedInitialSynchronizationRange);
} }
public void Reset() public void Reset()
@@ -70,6 +74,7 @@ public partial class WelcomeWizardContext : ObservableObject
SelectedProvider = null; SelectedProvider = null;
AccountName = null; AccountName = null;
AccountColorHex = null; AccountColorHex = null;
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
DisplayName = null; DisplayName = null;
EmailAddress = null; EmailAddress = null;
AppSpecificPassword = null; AppSpecificPassword = null;
@@ -997,7 +997,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SpecialImapProvider = _editingSpecialImapProvider, SpecialImapProvider = _editingSpecialImapProvider,
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled 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) if (serverInformation == null)
return false; return false;
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain; using Wino.Core.Domain;
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.Accounts;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels.Data; using Wino.Core.ViewModels.Data;
using Wino.Mail.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<IProviderDetail> Providers { get; private set; } = [];
public List<AppColorViewModel> AvailableColors { 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] [ObservableProperty]
public partial IProviderDetail SelectedProvider { get; set; } public partial IProviderDetail SelectedProvider { get; set; }
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsColorSelected))]
public partial AppColorViewModel SelectedColor { get; set; } public partial AppColorViewModel SelectedColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
[ObservableProperty] [ObservableProperty]
public partial string AccountName { get; set; } public partial string AccountName { get; set; }
@@ -36,6 +50,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
public partial bool CanProceed { get; set; } public partial bool CanProceed { get; set; }
public bool IsColorSelected => SelectedColor != null; public bool IsColorSelected => SelectedColor != null;
public bool IsInitialSynchronizationWarningVisible => SelectedInitialSynchronizationRange?.IsEverything == true;
public ProviderSelectionPageViewModel( public ProviderSelectionPageViewModel(
IProviderService providerService, IProviderService providerService,
@@ -45,6 +60,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
_providerService = providerService; _providerService = providerService;
_themeService = themeService; _themeService = themeService;
WizardContext = wizardContext; WizardContext = wizardContext;
SelectedInitialSynchronizationRange = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
} }
public override void OnNavigatedTo(NavigationMode mode, object parameters) public override void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -56,6 +72,10 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
.Select(hex => new AppColorViewModel(hex)) .Select(hex => new AppColorViewModel(hex))
.ToList(); .ToList();
SelectedInitialSynchronizationRange = InitialSynchronizationRanges
.FirstOrDefault(option => option.Range == WizardContext.SelectedInitialSynchronizationRange)
?? InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
// Restore from wizard context if navigating back // Restore from wizard context if navigating back
if (WizardContext.SelectedProvider != null) if (WizardContext.SelectedProvider != null)
{ {
@@ -71,9 +91,12 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
Validate(); Validate();
} }
partial void OnSelectedProviderChanged(IProviderDetail value) => Validate(); partial void OnSelectedProviderChanged(IProviderDetail value)
{
Validate();
}
partial void OnAccountNameChanged(string value) => Validate(); partial void OnAccountNameChanged(string value) => Validate();
partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected));
[RelayCommand] [RelayCommand]
private void ClearColor() => SelectedColor = null; private void ClearColor() => SelectedColor = null;
@@ -92,6 +115,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
WizardContext.SelectedProvider = SelectedProvider; WizardContext.SelectedProvider = SelectedProvider;
WizardContext.AccountName = AccountName?.Trim(); WizardContext.AccountName = AccountName?.Trim();
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty; WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
if (WizardContext.IsGenericImap) if (WizardContext.IsGenericImap)
{ {
@@ -55,6 +55,7 @@
<Grid MinWidth="400" RowSpacing="12"> <Grid MinWidth="400" RowSpacing="12">
<Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}"> <Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
@@ -96,6 +97,48 @@
</Grid> </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 <ListView
Grid.Row="2" Grid.Row="2"
@@ -15,7 +15,7 @@ namespace Wino.Mail.WinUI.Dialogs;
public sealed partial class NewAccountDialog : ContentDialog 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.iCloud, "https://support.apple.com/en-us/102654" },
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" }, { 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 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 static readonly DependencyProperty SelectedCalendarModeIndexProperty = DependencyProperty.Register(nameof(SelectedCalendarModeIndex), typeof(int), typeof(NewAccountDialog), new PropertyMetadata(0));
public AppColorViewModel? SelectedColor public AppColorViewModel? SelectedColor
{ {
get { return (AppColorViewModel?)GetValue(SelectedColorProperty); } get { return (AppColorViewModel?)GetValue(SelectedColorProperty); }
@@ -49,7 +48,6 @@ public sealed partial class NewAccountDialog : ContentDialog
set { SetValue(SelectedMailProviderProperty, value); } set { SetValue(SelectedMailProviderProperty, value); }
} }
public bool IsProviderSelectionVisible public bool IsProviderSelectionVisible
{ {
get { return (bool)GetValue(IsProviderSelectionVisibleProperty); } get { return (bool)GetValue(IsProviderSelectionVisibleProperty); }
@@ -63,10 +61,16 @@ public sealed partial class NewAccountDialog : ContentDialog
} }
// List of available mail providers for now. // List of available mail providers for now.
public List<IProviderDetail> Providers { get; set; } = []; public List<IProviderDetail> Providers { get; set; } = [];
public List<AppColorViewModel> AvailableColors { 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; } = public List<string> CalendarModeOptions { get; } =
[ [
Translator.ImapCalDavSettingsPage_CalendarModeCalDav, Translator.ImapCalDavSettingsPage_CalendarModeCalDav,
@@ -74,7 +78,6 @@ public sealed partial class NewAccountDialog : ContentDialog
Translator.ImapCalDavSettingsPage_CalendarModeDisabled Translator.ImapCalDavSettingsPage_CalendarModeDisabled
]; ];
public AccountCreationDialogResult? Result = null; public AccountCreationDialogResult? Result = null;
public NewAccountDialog() public NewAccountDialog()
@@ -85,6 +88,8 @@ public sealed partial class NewAccountDialog : ContentDialog
AvailableColors = themeService.Select(a => new AppColorViewModel(a)).ToList(); AvailableColors = themeService.Select(a => new AppColorViewModel(a)).ToList();
UpdateSelectedColor(); UpdateSelectedColor();
InitialSynchronizationComboBox.SelectedItem = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
UpdateInitialSynchronizationState();
} }
private static void OnSelectedProviderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) 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); 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) private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{ {
@@ -116,9 +134,11 @@ public sealed partial class NewAccountDialog : ContentDialog
if (SelectedMailProvider == null) if (SelectedMailProvider == null)
return; return;
var initialSynchronizationRange = GetInitialSynchronizationRange();
if (IsSpecialImapServerPartVisible) if (IsSpecialImapServerPartVisible)
{ {
// Special imap detail input. // Special IMAP detail input.
var calendarSupportMode = SelectedCalendarModeIndex switch var calendarSupportMode = SelectedCalendarModeIndex switch
{ {
1 => ImapCalendarSupportMode.LocalOnly, 1 => ImapCalendarSupportMode.LocalOnly,
@@ -132,7 +152,12 @@ public sealed partial class NewAccountDialog : ContentDialog
DisplayNameTextBox.Text.Trim(), DisplayNameTextBox.Text.Trim(),
SelectedMailProvider.SpecialImapProvider, SelectedMailProvider.SpecialImapProvider,
calendarSupportMode); 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(); Hide();
return; return;
@@ -140,11 +165,11 @@ public sealed partial class NewAccountDialog : ContentDialog
Validate(); Validate();
if (IsSecondaryButtonEnabled) if (IsPrimaryButtonEnabled)
{ {
if (SelectedMailProvider.SpecialImapProvider != SpecialImapProvider.None) 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; args.Cancel = true;
IsProviderSelectionVisible = false; IsProviderSelectionVisible = false;
@@ -154,7 +179,12 @@ public sealed partial class NewAccountDialog : ContentDialog
} }
else 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(); Hide();
} }
} }
@@ -167,6 +197,7 @@ public sealed partial class NewAccountDialog : ContentDialog
{ {
ValidateCreateButton(); ValidateCreateButton();
ValidateNames(); ValidateNames();
UpdateInitialSynchronizationState();
} }
// Returns whether we can create account or not. // 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 ImapPasswordChanged(object sender, RoutedEventArgs e) => Validate();
private void InitialSynchronizationSelectionChanged(object sender, SelectionChangedEventArgs e)
=> UpdateInitialSynchronizationState();
private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e) private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e)
{ {
if (SelectedMailProvider == null || if (SelectedMailProvider == null ||
@@ -210,6 +210,13 @@
</TransitionCollection> </TransitionCollection>
</StackPanel.ChildrenTransitions> </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 <controls:SettingsCard
Command="{x:Bind ViewModel.EditAliasesCommand}" Command="{x:Bind ViewModel.EditAliasesCommand}"
@@ -10,15 +10,12 @@
xmlns:helpers="using:Wino.Helpers" xmlns:helpers="using:Wino.Helpers"
xmlns:interfaces="using:Wino.Core.Domain.Interfaces" xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d"> mc:Ignorable="d">
<ScrollViewer <ScrollViewer HorizontalAlignment="Center" VerticalScrollBarVisibility="Auto">
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalScrollBarVisibility="Auto">
<StackPanel <StackPanel
MaxWidth="480" Margin="0,12"
Margin="0,24,0,24"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Spacing="20"> Spacing="20">
@@ -76,6 +73,50 @@
</Button> </Button>
</Grid> </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 --> <!-- Provider List -->
<ItemsView <ItemsView
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
+5
View File
@@ -590,6 +590,11 @@ public class AccountService : BaseDatabaseService, IAccountService
{ {
Guard.IsNotNull(account); Guard.IsNotNull(account);
if (!account.CreatedAt.HasValue)
{
account.CreatedAt = DateTime.UtcNow;
}
var accountCount = await Connection.Table<MailAccount>().CountAsync(); var accountCount = await Connection.Table<MailAccount>().CountAsync();
// If there are no accounts before this one, set it as startup account. // 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); 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); var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid))) if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
@@ -211,6 +211,8 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider, SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider,
AccountColorHex = mailbox.AccountColorHex?.Trim(), AccountColorHex = mailbox.AccountColorHex?.Trim(),
Base64ProfilePictureData = string.Empty, Base64ProfilePictureData = string.Empty,
CreatedAt = DateTime.UtcNow,
InitialSynchronizationRange = InitialSynchronizationRange.SixMonths,
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted, IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
SynchronizationDeltaIdentifier = string.Empty, SynchronizationDeltaIdentifier = string.Empty,
CalendarSynchronizationDeltaIdentifier = string.Empty, CalendarSynchronizationDeltaIdentifier = string.Empty,