Synchronizing calendars for gmail and some events.
This commit is contained in:
@@ -125,7 +125,7 @@ namespace Wino.Calendar.ViewModels
|
||||
Type = MailSynchronizationType.UpdateProfile
|
||||
};
|
||||
|
||||
var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
|
||||
var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
|
||||
|
||||
var profileSynchronizationResult = profileSynchronizationResponse.Data;
|
||||
|
||||
@@ -141,11 +141,13 @@ namespace Wino.Calendar.ViewModels
|
||||
accountCreationDialog.State = AccountCreationDialogState.FetchingEvents;
|
||||
|
||||
// Start synchronizing events.
|
||||
var eventsSyncOptions = new MailSynchronizationOptions()
|
||||
var synchronizationOptions = new CalendarSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = MailSynchronizationType.Events
|
||||
Type = CalendarSynchronizationType.CalendarMetadata
|
||||
};
|
||||
|
||||
var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -8,13 +7,14 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Collections;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.MenuItems;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
|
||||
namespace Wino.Calendar.ViewModels
|
||||
{
|
||||
@@ -22,18 +22,17 @@ namespace Wino.Calendar.ViewModels
|
||||
IRecipient<VisibleDateRangeChangedMessage>,
|
||||
IRecipient<CalendarEnableStatusChangedMessage>,
|
||||
IRecipient<CalendarInitializedMessage>,
|
||||
IRecipient<NavigateManageAccountsRequested>
|
||||
IRecipient<NavigateManageAccountsRequested>,
|
||||
IRecipient<GoToCalendarDayMessage>
|
||||
{
|
||||
public event EventHandler<CalendarDisplayType> DisplayTypeChanged;
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
public IStatePersistanceService StatePersistenceService { get; }
|
||||
public INavigationService NavigationService { get; }
|
||||
public IWinoServerConnectionManager ServerConnectionManager { get; }
|
||||
public MenuItemCollection FooterItems { get; set; }
|
||||
public MenuItemCollection MenuItems { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private IMenuItem _selectedMenuItem;
|
||||
private int _selectedMenuItemIndex = -1;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCalendarEnabled;
|
||||
@@ -62,10 +61,13 @@ namespace Wino.Calendar.ViewModels
|
||||
[ObservableProperty]
|
||||
private int _selectedDateNavigationHeaderIndex;
|
||||
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
|
||||
|
||||
public AppShellViewModel(IPreferencesService preferencesService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IAccountService accountService,
|
||||
INavigationService navigationService,
|
||||
IWinoServerConnectionManager serverConnectionManager)
|
||||
{
|
||||
@@ -74,6 +76,7 @@ namespace Wino.Calendar.ViewModels
|
||||
PreferencesService = preferencesService;
|
||||
|
||||
StatePersistenceService = statePersistanceService;
|
||||
_accountService = accountService;
|
||||
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
|
||||
}
|
||||
|
||||
@@ -93,20 +96,37 @@ namespace Wino.Calendar.ViewModels
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
CreateFooterItems();
|
||||
UpdateDateNavigationHeaderItems();
|
||||
}
|
||||
|
||||
partial void OnSelectedMenuItemChanged(IMenuItem oldValue, IMenuItem newValue)
|
||||
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue)
|
||||
{
|
||||
if (newValue is SettingsItem)
|
||||
switch (newValue)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.SettingsPage);
|
||||
case -1:
|
||||
NavigationService.Navigate(WinoPage.CalendarPage);
|
||||
break;
|
||||
case 0:
|
||||
NavigationService.Navigate(WinoPage.AccountManagementPage);
|
||||
break;
|
||||
case 1:
|
||||
NavigationService.Navigate(WinoPage.SettingsPage);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
else if (newValue is ManageAccountsMenuItem)
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Sync()
|
||||
{
|
||||
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.AccountManagementPage);
|
||||
}
|
||||
AccountId = Guid.Parse("52fae547-0740-4aa3-8d51-519bd31278ca"),
|
||||
Type = CalendarSynchronizationType.CalendarMetadata
|
||||
}, SynchronizationSource.Client);
|
||||
|
||||
Messenger.Send(t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -142,14 +162,6 @@ namespace Wino.Calendar.ViewModels
|
||||
return DateTime.Today.Date;
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
|
||||
MenuItems = new MenuItemCollection(Dispatcher);
|
||||
FooterItems = new MenuItemCollection(Dispatcher);
|
||||
}
|
||||
|
||||
public override void OnPageLoaded()
|
||||
{
|
||||
base.OnPageLoaded();
|
||||
@@ -160,13 +172,6 @@ namespace Wino.Calendar.ViewModels
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateFooterItems()
|
||||
{
|
||||
FooterItems.Clear();
|
||||
FooterItems.Add(new ManageAccountsMenuItem());
|
||||
FooterItems.Add(new SettingsItem());
|
||||
}
|
||||
|
||||
#region Commands
|
||||
|
||||
[RelayCommand]
|
||||
@@ -236,6 +241,8 @@ namespace Wino.Calendar.ViewModels
|
||||
// Calendar page is loaded and calendar is ready to recieve render requests.
|
||||
public void Receive(CalendarInitializedMessage message) => Messenger.Send(new GoToCalendarDayMessage(DateTime.Now.Date));
|
||||
|
||||
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItem = FooterItems.FirstOrDefault(a => a is ManageAccountsMenuItem);
|
||||
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
|
||||
|
||||
public void Receive(GoToCalendarDayMessage message) => SelectedMenuItemIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,62 +375,62 @@ namespace Wino.Calendar.ViewModels
|
||||
base.OnCalendarEventAdded(calendarItem);
|
||||
|
||||
// Test
|
||||
var eventDays = DayRanges.SelectMany(a => a.CalendarDays).Where(b => b.Period.Start.Date == calendarItem.StartTime.Date);
|
||||
//var eventDays = DayRanges.SelectMany(a => a.CalendarDays).Where(b => b.Period.Start.Date == calendarItem.StartTime.Date);
|
||||
|
||||
var beforeAllDay = new CalendarItem(calendarItem.StartTime.Date.AddHours(0), calendarItem.StartTime.Date.AddMinutes(30))
|
||||
{
|
||||
Title = "kj"
|
||||
};
|
||||
//var beforeAllDay = new CalendarItem(calendarItem.StartTime.Date.AddHours(0), calendarItem.StartTime.Date.AddMinutes(30))
|
||||
//{
|
||||
// Title = "kj"
|
||||
//};
|
||||
|
||||
var allday = new CalendarItem(calendarItem.StartTime.Date.AddHours(1), calendarItem.StartTime.Date.AddHours(10).AddMinutes(59))
|
||||
{
|
||||
Title = "All day"
|
||||
};
|
||||
//var allday = new CalendarItem(calendarItem.StartTime.Date.AddHours(1), calendarItem.StartTime.Date.AddHours(10).AddMinutes(59))
|
||||
//{
|
||||
// Title = "All day"
|
||||
//};
|
||||
|
||||
var test = new CalendarItem(calendarItem.StartTime.Date.AddHours(4), calendarItem.StartTime.Date.AddHours(4).AddMinutes(30))
|
||||
{
|
||||
Title = "test"
|
||||
};
|
||||
//var test = new CalendarItem(calendarItem.StartTime.Date.AddHours(4), calendarItem.StartTime.Date.AddHours(4).AddMinutes(30))
|
||||
//{
|
||||
// Title = "test"
|
||||
//};
|
||||
|
||||
var hour = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(8))
|
||||
{
|
||||
Title = "1 h"
|
||||
};
|
||||
//var hour = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(8))
|
||||
//{
|
||||
// Title = "1 h"
|
||||
//};
|
||||
|
||||
var hourandhalf = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(8).AddMinutes(30))
|
||||
{
|
||||
Title = "1.5 h"
|
||||
};
|
||||
var halfhour1 = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(7).AddMinutes(30))
|
||||
{
|
||||
Title = "30 min"
|
||||
};
|
||||
//var hourandhalf = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(8).AddMinutes(30))
|
||||
//{
|
||||
// Title = "1.5 h"
|
||||
//};
|
||||
//var halfhour1 = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(7).AddMinutes(30))
|
||||
//{
|
||||
// Title = "30 min"
|
||||
//};
|
||||
|
||||
var halfhour2 = new CalendarItem(calendarItem.StartTime.Date.AddHours(7).AddMinutes(30), calendarItem.StartTime.Date.AddHours(8))
|
||||
{
|
||||
Title = "30 min"
|
||||
};
|
||||
var halfhour3 = new CalendarItem(calendarItem.StartTime.Date.AddHours(8), calendarItem.StartTime.Date.AddHours(8).AddMinutes(30))
|
||||
{
|
||||
Title = "30 min"
|
||||
};
|
||||
//var halfhour2 = new CalendarItem(calendarItem.StartTime.Date.AddHours(7).AddMinutes(30), calendarItem.StartTime.Date.AddHours(8))
|
||||
//{
|
||||
// Title = "30 min"
|
||||
//};
|
||||
//var halfhour3 = new CalendarItem(calendarItem.StartTime.Date.AddHours(8), calendarItem.StartTime.Date.AddHours(8).AddMinutes(30))
|
||||
//{
|
||||
// Title = "30 min"
|
||||
//};
|
||||
|
||||
foreach (var day in eventDays)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
day.Events.Add(beforeAllDay);
|
||||
day.Events.Add(allday);
|
||||
day.Events.Add(hourandhalf);
|
||||
day.Events.Add(hour);
|
||||
day.Events.Add(halfhour1);
|
||||
day.Events.Add(halfhour2);
|
||||
day.Events.Add(halfhour3);
|
||||
day.Events.Add(test);
|
||||
});
|
||||
}
|
||||
//foreach (var day in eventDays)
|
||||
//{
|
||||
// await ExecuteUIThread(() =>
|
||||
// {
|
||||
// day.Events.Add(beforeAllDay);
|
||||
// day.Events.Add(allday);
|
||||
// day.Events.Add(hourandhalf);
|
||||
// day.Events.Add(hour);
|
||||
// day.Events.Add(halfhour1);
|
||||
// day.Events.Add(halfhour2);
|
||||
// day.Events.Add(halfhour3);
|
||||
// day.Events.Add(test);
|
||||
// });
|
||||
//}
|
||||
|
||||
return;
|
||||
//return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ using Wino.Activation;
|
||||
using Wino.Calendar.Activation;
|
||||
using Wino.Calendar.Services;
|
||||
using Wino.Calendar.ViewModels;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.UWP;
|
||||
using Wino.Messaging.Client.Connection;
|
||||
using Wino.Messaging.Server;
|
||||
@@ -20,7 +24,7 @@ using Wino.Services;
|
||||
|
||||
namespace Wino.Calendar
|
||||
{
|
||||
public sealed partial class App : WinoApplication, IRecipient<NewSynchronizationRequested>
|
||||
public sealed partial class App : WinoApplication, IRecipient<NewCalendarSynchronizationRequested>
|
||||
{
|
||||
public override string AppCenterKey => "dfdad6ab-95f9-44cc-9112-45ec6730c49e";
|
||||
|
||||
@@ -101,13 +105,6 @@ namespace Wino.Calendar
|
||||
protected override ActivationHandler<IActivatedEventArgs> GetDefaultActivationHandler()
|
||||
=> new DefaultActivationHandler();
|
||||
|
||||
public void Receive(NewSynchronizationRequested message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
|
||||
{
|
||||
base.OnBackgroundActivated(args);
|
||||
@@ -142,5 +139,20 @@ namespace Wino.Calendar
|
||||
|
||||
AppServiceConnectionManager.Connection = null;
|
||||
}
|
||||
|
||||
public async void Receive(NewCalendarSynchronizationRequested message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(message);
|
||||
synchronizationResultResponse.ThrowIfFailed();
|
||||
}
|
||||
catch (WinoServerException serverException)
|
||||
{
|
||||
var dialogService = Services.GetService<ICalendarDialogService>();
|
||||
|
||||
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,9 @@ namespace Wino.Calendar.Controls
|
||||
|
||||
var childMeasurement = _measurements[child.Item.Id];
|
||||
|
||||
double childHeight = Math.Max(0, GetChildHeight(child.Item.StartTime, child.Item.EndTime));
|
||||
// TODO Math.Max(0, GetChildHeight(child.Item.StartTime, child.Item.EndTime));
|
||||
// Recurring events may not have an end time. We need to calculate the height based on the start time and duration.
|
||||
double childHeight = 50;
|
||||
double childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
|
||||
double childTop = Math.Max(0, GetChildTopMargin(child.Item.StartTime, availableHeight));
|
||||
double childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<SolidColorBrush x:Key="CalendarFieldSelectedBackgroundBrush">#121212</SolidColorBrush>
|
||||
|
||||
<SolidColorBrush x:Key="WinoCalendarViewBorderBrush">#3d3d3d</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="WinoCalendarViewVisibleDayBackgroundBrush">#30336b</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="WinoCalendarViewVisibleDayBackgroundBrush">#4b4b4b</SolidColorBrush>
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Wino.Calendar.Args;
|
||||
using Wino.Calendar.Views.Abstract;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Views
|
||||
@@ -30,24 +29,15 @@ namespace Wino.Calendar.Views
|
||||
{
|
||||
selectedDateTime = e.ClickedDate;
|
||||
|
||||
// TODO: Popup is not positioned well on daily view.
|
||||
TeachingTipPositionerGrid.Width = e.CellSize.Width;
|
||||
TeachingTipPositionerGrid.Height = e.CellSize.Height;
|
||||
|
||||
Canvas.SetLeft(TeachingTipPositionerGrid, e.PositionerPoint.X);
|
||||
Canvas.SetTop(TeachingTipPositionerGrid, e.PositionerPoint.Y);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarEventAdded(new CalendarItem(selectedDateTime.Value, default)));
|
||||
|
||||
//var t = new Flyout()
|
||||
//{
|
||||
// Content = new TextBlock() { Text = "Create event" }
|
||||
//};
|
||||
|
||||
//t.ShowAt(TeachingTipPositionerGrid, new FlyoutShowOptions()
|
||||
//{
|
||||
// ShowMode = FlyoutShowMode.Transient,
|
||||
// Placement = FlyoutPlacementMode.Right
|
||||
//});
|
||||
// TODO: End time can be from settings.
|
||||
// WeakReferenceMessenger.Default.Send(new CalendarEventAdded(new CalendarItem(selectedDateTime.Value, selectedDateTime.Value.AddMinutes(30))));
|
||||
|
||||
NewEventTip.IsOpen = true;
|
||||
}
|
||||
@@ -71,7 +61,7 @@ namespace Wino.Calendar.Views
|
||||
var eventEndDate = selectedDateTime.Value.Add(EventTimePicker.Time);
|
||||
|
||||
// Create the event.
|
||||
WeakReferenceMessenger.Default.Send(new CalendarEventAdded(new CalendarItem(selectedDateTime.Value, eventEndDate)));
|
||||
// WeakReferenceMessenger.Default.Send(new CalendarEventAdded(new CalendarItem(selectedDateTime.Value, eventEndDate)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ namespace Wino.Core.Domain.Entities.Calendar
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string RemoteCalendarId { get; set; }
|
||||
public string SynchronizationDeltaToken { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string ColorHex { get; set; }
|
||||
public string TimeZoneId { get; set; }
|
||||
public string TimeZone { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar
|
||||
{
|
||||
public class Attendee
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
public Guid EventId { get; set; }
|
||||
public Guid ContactId { get; set; }
|
||||
public AttendeeStatus Status { get; set; }
|
||||
public bool IsOrganizer { get; set; }
|
||||
}
|
||||
}
|
||||
20
Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs
Normal file
20
Wino.Core.Domain/Entities/Calendar/CalendarEventAttendee.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar
|
||||
{
|
||||
// TODO: Connect to Contact store with Wino People.
|
||||
public class CalendarEventAttendee
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
public Guid CalendarItemId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public AttendeeStatus AttendenceStatus { get; set; }
|
||||
public bool IsOrganizer { get; set; }
|
||||
public bool IsOptionalAttendee { get; set; }
|
||||
public string Comment { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,8 @@ namespace Wino.Core.Domain.Entities.Calendar
|
||||
public string Description { get; set; }
|
||||
public string Location { get; set; }
|
||||
public DateTimeOffset StartTime { get; set; }
|
||||
public DateTimeOffset EndTime { get; set; }
|
||||
public bool IsAllDay { get; set; }
|
||||
public Guid? RecurrenceRuleId { get; set; }
|
||||
public int DurationInMinutes { get; set; }
|
||||
public string Recurrence { get; set; }
|
||||
public CalendarItemStatus Status { get; set; }
|
||||
public CalendarItemVisibility Visibility { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
@@ -24,6 +23,6 @@ namespace Wino.Core.Domain.Entities.Calendar
|
||||
public Guid CalendarId { get; set; }
|
||||
|
||||
[Ignore]
|
||||
public TimeRange Period => new TimeRange(StartTime.Date, EndTime.Date);
|
||||
public TimeRange Period => new TimeRange(StartTime.Date, StartTime.Date.AddMinutes(DurationInMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar
|
||||
{
|
||||
public class RecurrenceRule
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid CalendarItemId { get; set; }
|
||||
public CalendarItemRecurrenceFrequency Frequency { get; set; }
|
||||
public int Interval { get; set; }
|
||||
public string DaysOfWeek { get; set; }
|
||||
public DateTimeOffset StartDate { get; set; }
|
||||
public DateTimeOffset EndDate { get; set; }
|
||||
public int Occurrences { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace Wino.Core.Domain.Entities.Shared
|
||||
public MailProviderType ProviderType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For tracking change delta.
|
||||
/// For tracking mail change delta.
|
||||
/// Gmail : historyId
|
||||
/// Outlook: deltaToken
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public enum AttendeeStatus
|
||||
{
|
||||
NeedsAction,
|
||||
Accepted,
|
||||
Tentative,
|
||||
Declined
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
{
|
||||
public enum CalendarItemVisibility
|
||||
{
|
||||
Default,
|
||||
Public,
|
||||
Private
|
||||
Private,
|
||||
Confidential
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
{
|
||||
public enum CalendarSynchronizationType
|
||||
{
|
||||
AllCalendars, // Sync all calendars.
|
||||
SingleCalendar, // Sync only one calendar.
|
||||
ExecuteRequests, // Execute all requests in the queue.
|
||||
CalendarMetadata, // Sync calendar metadata.
|
||||
CalendarEvents, // Sync all events for all calendars.
|
||||
SingleCalendar, // Sync events for only specified calendars.
|
||||
UpdateProfile // Update profile information only.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
string Title { get; }
|
||||
Guid Id { get; }
|
||||
DateTimeOffset StartTime { get; }
|
||||
DateTimeOffset EndTime { get; }
|
||||
int DurationInMinutes { get; }
|
||||
TimeRange Period { get; }
|
||||
}
|
||||
}
|
||||
|
||||
18
Wino.Core.Domain/Interfaces/ICalendarService.cs
Normal file
18
Wino.Core.Domain/Interfaces/ICalendarService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface ICalendarService
|
||||
{
|
||||
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
|
||||
Task DeleteCalendarItemAsync(Guid calendarItemId);
|
||||
|
||||
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,6 @@ namespace Wino.Core.Domain.Interfaces
|
||||
Task<List<AccountContact>> GetAddressInformationAsync(string queryText);
|
||||
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
|
||||
Task SaveAddressInformationAsync(MimeMessage message);
|
||||
Task<AccountContact> CreateNewContactAsync(string address, string displayName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// <returns>Result summary of synchronization.</returns>
|
||||
Task<MailSynchronizationResult> SynchronizeMailsAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a single MIME message from the server and saves it to disk.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using Itenso.TimePeriod;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar
|
||||
{
|
||||
public class CalendarItem : ICalendarItem
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public CalendarItem(DateTime startTime, DateTime endTime)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Period = new TimeRange(startTime, endTime);
|
||||
}
|
||||
|
||||
public DateTimeOffset StartTime { get; }
|
||||
public DateTimeOffset EndTime { get; }
|
||||
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
|
||||
public TimeRange Period { get; }
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace Wino.Helpers
|
||||
{
|
||||
private const string TwentyFourHourTimeFormat = "HH:mm";
|
||||
private const string TwelveHourTimeFormat = "hh:mm tt";
|
||||
|
||||
#region Converters
|
||||
|
||||
public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible;
|
||||
@@ -40,6 +41,19 @@ namespace Wino.Helpers
|
||||
_ => InfoBarSeverity.Informational,
|
||||
};
|
||||
}
|
||||
|
||||
public static Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode NavigationViewDisplayModeConverter(SplitViewDisplayMode splitViewDisplayMode)
|
||||
{
|
||||
return splitViewDisplayMode switch
|
||||
{
|
||||
SplitViewDisplayMode.CompactOverlay => Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Compact,
|
||||
SplitViewDisplayMode.CompactInline => Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Minimal,
|
||||
SplitViewDisplayMode.Overlay => Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Expanded,
|
||||
SplitViewDisplayMode.Inline => Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Expanded,
|
||||
_ => Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Minimal,
|
||||
};
|
||||
}
|
||||
|
||||
public static SolidColorBrush GetSolidColorBrushFromHex(string colorHex) => string.IsNullOrEmpty(colorHex) ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(colorHex.ToColor());
|
||||
public static Visibility IsSelectionModeMultiple(ListViewSelectionMode mode) => mode == ListViewSelectionMode.Multiple ? Visibility.Visible : Visibility.Collapsed;
|
||||
public static FontWeight GetFontWeightBySyncState(bool isSyncing) => isSyncing ? FontWeights.SemiBold : FontWeights.Normal;
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using Google.Apis.Gmail.v1.Data;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Services;
|
||||
@@ -13,10 +15,6 @@ namespace Wino.Core.Extensions
|
||||
{
|
||||
public static class GoogleIntegratorExtensions
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
private static string GetNormalizedLabelName(string labelName)
|
||||
{
|
||||
// 1. Remove CATEGORY_ prefix.
|
||||
@@ -168,5 +166,119 @@ namespace Wino.Core.Extensions
|
||||
IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static AccountCalendar AsCalendar(this CalendarListEntry calendarListEntry, Guid accountId)
|
||||
{
|
||||
return new AccountCalendar()
|
||||
{
|
||||
RemoteCalendarId = calendarListEntry.Id,
|
||||
AccountId = accountId,
|
||||
Name = calendarListEntry.Summary,
|
||||
Id = Guid.NewGuid(),
|
||||
TimeZone = calendarListEntry.TimeZone,
|
||||
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the start DateTimeOffset of a Google Calendar Event.
|
||||
/// Handles different date/time representations (date-only, date-time, recurring events).
|
||||
/// Uses the DateTimeDateTimeOffset property for optimal performance and accuracy.
|
||||
/// </summary>
|
||||
/// <param name="calendarEvent">The Google Calendar Event object.</param>
|
||||
/// <returns>The start DateTimeOffset of the event, or null if it cannot be determined.</returns>
|
||||
public static DateTimeOffset? GetEventStartDateTimeOffset(this Event calendarEvent)
|
||||
{
|
||||
if (calendarEvent == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (calendarEvent.Start != null)
|
||||
{
|
||||
if (calendarEvent.Start.DateTimeDateTimeOffset != null)
|
||||
{
|
||||
return calendarEvent.Start.DateTimeDateTimeOffset; // Use the direct DateTimeOffset property!
|
||||
}
|
||||
else if (calendarEvent.Start.Date != null)
|
||||
{
|
||||
if (DateTime.TryParse(calendarEvent.Start.Date, out DateTime startDate))
|
||||
{
|
||||
// Date-only events are treated as UTC midnight
|
||||
return new DateTimeOffset(startDate, TimeSpan.Zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Start time not found
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the duration of a Google Calendar Event in minutes.
|
||||
/// Handles date-only and date-time events, but *does not* handle recurring events correctly.
|
||||
/// For recurring events, this method will return the duration of the *first* instance.
|
||||
/// </summary>
|
||||
/// <param name="calendarEvent">The Google Calendar Event object.</param>
|
||||
/// <returns>The duration of the event in minutes, or null if it cannot be determined.</returns>
|
||||
public static int? GetEventDurationInMinutes(this Event calendarEvent)
|
||||
{
|
||||
if (calendarEvent == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTimeOffset? start = calendarEvent.GetEventStartDateTimeOffset();
|
||||
if (start == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTimeOffset? end = null;
|
||||
if (calendarEvent.End != null)
|
||||
{
|
||||
if (calendarEvent.End.DateTimeDateTimeOffset != null)
|
||||
{
|
||||
end = calendarEvent.End.DateTimeDateTimeOffset;
|
||||
}
|
||||
else if (calendarEvent.End.Date != null)
|
||||
{
|
||||
if (DateTime.TryParse(calendarEvent.End.Date, out DateTime endDate))
|
||||
{
|
||||
end = new DateTimeOffset(endDate, TimeSpan.Zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (end == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)(end.Value - start.Value).TotalMinutes;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns>___ separated lines.</returns>
|
||||
public static string GetRecurrenceString(this Event calendarEvent)
|
||||
{
|
||||
if (calendarEvent == null || calendarEvent.Recurrence == null || !calendarEvent.Recurrence.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return string.Join("___", calendarEvent.Recurrence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
@@ -32,28 +34,28 @@ namespace Wino.Core.Integration.Processors
|
||||
Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
|
||||
Task InsertFolderAsync(MailItemFolder folder);
|
||||
Task UpdateFolderAsync(MailItemFolder folder);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of folders that are available for account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account id to get folders for.</param>
|
||||
/// <returns>All folders.</returns>
|
||||
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
|
||||
|
||||
|
||||
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(MailSynchronizationOptions options);
|
||||
|
||||
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
|
||||
Task UpdateFolderLastSyncDateAsync(Guid folderId);
|
||||
|
||||
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
|
||||
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
|
||||
|
||||
// Calendar
|
||||
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
|
||||
|
||||
Task DeleteCalendarItemAsync(Guid calendarItemId);
|
||||
|
||||
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
}
|
||||
|
||||
public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
||||
{
|
||||
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
|
||||
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
||||
Task<CalendarItem> CreateCalendarItemAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
|
||||
}
|
||||
|
||||
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
|
||||
@@ -115,13 +117,15 @@ namespace Wino.Core.Integration.Processors
|
||||
public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
ICalendarService calendarService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
|
||||
{
|
||||
protected IMailService MailService = mailService;
|
||||
|
||||
protected ICalendarService CalendarService = calendarService;
|
||||
protected IFolderService FolderService = folderService;
|
||||
protected IAccountService AccountService = accountService;
|
||||
|
||||
private readonly IMimeFileService _mimeFileService = mimeFileService;
|
||||
|
||||
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
|
||||
@@ -179,5 +183,20 @@ namespace Wino.Core.Integration.Processors
|
||||
|
||||
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
||||
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
|
||||
|
||||
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
||||
=> CalendarService.GetAccountCalendarsAsync(accountId);
|
||||
|
||||
public Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
=> CalendarService.DeleteCalendarItemAsync(calendarItemId);
|
||||
|
||||
public Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
=> CalendarService.DeleteAccountCalendarAsync(accountCalendar);
|
||||
|
||||
public Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
=> CalendarService.InsertAccountCalendarAsync(accountCalendar);
|
||||
|
||||
public Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
=> CalendarService.UpdateAccountCalendarAsync(accountCalendar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Services;
|
||||
using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee;
|
||||
using CalendarItem = Wino.Core.Domain.Entities.Calendar.CalendarItem;
|
||||
|
||||
namespace Wino.Core.Integration.Processors
|
||||
{
|
||||
public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcessor
|
||||
{
|
||||
public GmailChangeProcessor(IDatabaseService databaseService, IFolderService folderService, IMailService mailService, IAccountService accountService, IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, accountService, mimeFileService)
|
||||
public GmailChangeProcessor(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
ICalendarService calendarService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId)
|
||||
@@ -16,5 +30,116 @@ namespace Wino.Core.Integration.Processors
|
||||
|
||||
public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
||||
=> MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
|
||||
|
||||
public async Task<CalendarItem> CreateCalendarItemAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
|
||||
{
|
||||
var calendarItem = new CalendarItem()
|
||||
{
|
||||
CalendarId = assignedCalendar.Id,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Description = calendarEvent.Description,
|
||||
StartTime = GoogleIntegratorExtensions.GetEventStartDateTimeOffset(calendarEvent) ?? throw new Exception("Event without a start time."),
|
||||
DurationInMinutes = GoogleIntegratorExtensions.GetEventDurationInMinutes(calendarEvent) ?? throw new Exception("Event without a duration."),
|
||||
Id = Guid.NewGuid(),
|
||||
Location = calendarEvent.Location,
|
||||
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||
Status = GetStatus(calendarEvent.Status),
|
||||
Title = calendarEvent.Summary,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Visibility = GetVisibility(calendarEvent.Visibility),
|
||||
};
|
||||
|
||||
// TODO: There are some edge cases with cancellation here.
|
||||
CalendarItemStatus GetStatus(string status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
"confirmed" => CalendarItemStatus.Confirmed,
|
||||
"tentative" => CalendarItemStatus.Tentative,
|
||||
"cancelled" => CalendarItemStatus.Cancelled,
|
||||
_ => CalendarItemStatus.Confirmed
|
||||
};
|
||||
}
|
||||
|
||||
CalendarItemVisibility GetVisibility(string visibility)
|
||||
{
|
||||
/// Visibility of the event. Optional. Possible values are: - "default" - Uses the default visibility for
|
||||
/// events on the calendar. This is the default value. - "public" - The event is public and event details are
|
||||
/// visible to all readers of the calendar. - "private" - The event is private and only event attendees may
|
||||
/// view event details. - "confidential" - The event is private. This value is provided for compatibility
|
||||
/// reasons.
|
||||
|
||||
return visibility switch
|
||||
{
|
||||
"default" => CalendarItemVisibility.Default,
|
||||
"public" => CalendarItemVisibility.Public,
|
||||
"private" => CalendarItemVisibility.Private,
|
||||
"confidential" => CalendarItemVisibility.Confidential,
|
||||
_ => CalendarItemVisibility.Default
|
||||
};
|
||||
}
|
||||
|
||||
// Attendees
|
||||
var attendees = new List<CalendarEventAttendee>();
|
||||
|
||||
if (calendarEvent.Attendees == null)
|
||||
{
|
||||
// Self-only event.
|
||||
|
||||
attendees.Add(new CalendarEventAttendee()
|
||||
{
|
||||
CalendarItemId = calendarItem.Id,
|
||||
IsOrganizer = true,
|
||||
Email = organizerAccount.Address,
|
||||
Name = organizerAccount.SenderName,
|
||||
AttendenceStatus = AttendeeStatus.Accepted,
|
||||
Id = Guid.NewGuid(),
|
||||
IsOptionalAttendee = false,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var attendee in calendarEvent.Attendees)
|
||||
{
|
||||
if (attendee.Self == true)
|
||||
{
|
||||
// TODO:
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(attendee.Email))
|
||||
{
|
||||
AttendeeStatus GetAttendenceStatus(string responseStatus)
|
||||
{
|
||||
return responseStatus switch
|
||||
{
|
||||
"accepted" => AttendeeStatus.Accepted,
|
||||
"declined" => AttendeeStatus.Declined,
|
||||
"tentative" => AttendeeStatus.Tentative,
|
||||
"needsAction" => AttendeeStatus.NeedsAction,
|
||||
_ => AttendeeStatus.NeedsAction
|
||||
};
|
||||
}
|
||||
|
||||
var eventAttendee = new CalendarEventAttendee()
|
||||
{
|
||||
CalendarItemId = calendarItem.Id,
|
||||
IsOrganizer = attendee.Organizer ?? false,
|
||||
Comment = attendee.Comment,
|
||||
Email = attendee.Email,
|
||||
Name = attendee.DisplayName,
|
||||
AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
|
||||
Id = Guid.NewGuid(),
|
||||
IsOptionalAttendee = attendee.Optional ?? false,
|
||||
};
|
||||
|
||||
attendees.Add(eventAttendee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
|
||||
|
||||
return calendarItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace Wino.Core.Integration.Processors
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, accountService, mimeFileService)
|
||||
ICalendarService calendarService,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ namespace Wino.Core.Integration.Processors
|
||||
{
|
||||
public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
ICalendarService calendarService,
|
||||
IMailService mailService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, accountService, mimeFileService)
|
||||
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
, IOutlookChangeProcessor
|
||||
{
|
||||
public Task<bool> IsMailExistsAsync(string messageId)
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace Wino.Core.Services
|
||||
Type = MailSynchronizationType.ExecuteRequests
|
||||
};
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client));
|
||||
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
|
||||
}
|
||||
|
||||
private async Task EnsureServerConnectedAsync()
|
||||
|
||||
@@ -17,6 +17,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using MimeKit;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -33,6 +34,7 @@ using Wino.Core.Requests.Folder;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Messaging.UI;
|
||||
using Wino.Services;
|
||||
using CalendarService = Google.Apis.Calendar.v3.CalendarService;
|
||||
|
||||
namespace Wino.Core.Synchronizers.Mail
|
||||
{
|
||||
@@ -47,6 +49,7 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
|
||||
private readonly ConfigurableHttpClient _googleHttpClient;
|
||||
private readonly GmailService _gmailService;
|
||||
private readonly CalendarService _calendarService;
|
||||
private readonly PeopleServiceService _peopleService;
|
||||
|
||||
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
||||
@@ -64,9 +67,10 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
};
|
||||
|
||||
_googleHttpClient = new ConfigurableHttpClient(messageHandler);
|
||||
|
||||
_gmailService = new GmailService(initializer);
|
||||
_peopleService = new PeopleServiceService(initializer);
|
||||
_calendarService = new CalendarService(initializer);
|
||||
|
||||
_gmailChangeProcessor = gmailChangeProcessor;
|
||||
}
|
||||
|
||||
@@ -284,109 +288,258 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
return MailSynchronizationResult.Completed(unreadNewItems);
|
||||
}
|
||||
|
||||
protected override Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
protected override async Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.Information("Internal calendar synchronization started for {Name}", Account.Name);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
bool isInitialSync = string.IsNullOrEmpty(Account.SynchronizationDeltaIdentifier);
|
||||
|
||||
_logger.Debug("Is initial synchronization: {IsInitialSync}", isInitialSync);
|
||||
|
||||
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||
|
||||
// TODO: Better logging and exception handling.
|
||||
foreach (var calendar in localCalendars)
|
||||
{
|
||||
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
|
||||
|
||||
request.SingleEvents = false;
|
||||
request.ShowDeleted = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
|
||||
{
|
||||
// If a sync token is available, perform an incremental sync
|
||||
request.SyncToken = calendar.SynchronizationDeltaToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no sync token, perform an initial sync
|
||||
// Fetch events from the past year
|
||||
|
||||
request.TimeMinDateTimeOffset = DateTimeOffset.UtcNow.AddYears(-1);
|
||||
}
|
||||
|
||||
string nextPageToken;
|
||||
string syncToken;
|
||||
|
||||
var allEvents = new List<Event>();
|
||||
|
||||
do
|
||||
{
|
||||
// Execute the request
|
||||
var events = await request.ExecuteAsync();
|
||||
|
||||
// Process the fetched events
|
||||
if (events.Items != null)
|
||||
{
|
||||
allEvents.AddRange(events.Items);
|
||||
}
|
||||
|
||||
// Get the next page token and sync token
|
||||
nextPageToken = events.NextPageToken;
|
||||
syncToken = events.NextSyncToken;
|
||||
|
||||
// Set the next page token for subsequent requests
|
||||
request.PageToken = nextPageToken;
|
||||
|
||||
} while (!string.IsNullOrEmpty(nextPageToken));
|
||||
|
||||
calendar.SynchronizationDeltaToken = syncToken;
|
||||
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
|
||||
foreach (var @event in allEvents)
|
||||
{
|
||||
// TODO: Exception handling for event processing.
|
||||
await _gmailChangeProcessor.CreateCalendarItemAsync(@event, calendar, Account).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
var calendarListRequest = _calendarService.CalendarList.List();
|
||||
var calendarListResponse = await calendarListRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (calendarListResponse.Items == null)
|
||||
{
|
||||
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var folderRequest = _gmailService.Users.Labels.List("me");
|
||||
_logger.Warning("No calendars found for {Name}", Account.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var labelsResponse = await folderRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||
|
||||
if (labelsResponse.Labels == null)
|
||||
List<AccountCalendar> insertedCalendars = new();
|
||||
List<AccountCalendar> updatedCalendars = new();
|
||||
List<AccountCalendar> deletedCalendars = new();
|
||||
|
||||
// 1. Handle deleted calendars.
|
||||
|
||||
foreach (var calendar in localCalendars)
|
||||
{
|
||||
var remoteCalendar = calendarListResponse.Items.FirstOrDefault(a => a.Id == calendar.RemoteCalendarId);
|
||||
if (remoteCalendar == null)
|
||||
{
|
||||
_logger.Warning("No folders found for {Name}", Account.Name);
|
||||
return;
|
||||
// Local calendar doesn't exists remotely. Delete local copy.
|
||||
|
||||
await _gmailChangeProcessor.DeleteAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
deletedCalendars.Add(calendar);
|
||||
}
|
||||
}
|
||||
|
||||
List<MailItemFolder> insertedFolders = new();
|
||||
List<MailItemFolder> updatedFolders = new();
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
// Delete the deleted folders from local list.
|
||||
deletedCalendars.ForEach(a => localCalendars.Remove(a));
|
||||
|
||||
// 1. Handle deleted labels.
|
||||
|
||||
foreach (var localFolder in localFolders)
|
||||
// 2. Handle update/insert based on remote calendars.
|
||||
foreach (var calendar in calendarListResponse.Items)
|
||||
{
|
||||
var existingLocalCalendar = localCalendars.FirstOrDefault(a => a.RemoteCalendarId == calendar.Id);
|
||||
if (existingLocalCalendar == null)
|
||||
{
|
||||
// Category folder is virtual folder for Wino. Skip it.
|
||||
if (localFolder.SpecialFolderType == SpecialFolderType.Category) continue;
|
||||
|
||||
var remoteFolder = labelsResponse.Labels.FirstOrDefault(a => a.Id == localFolder.RemoteFolderId);
|
||||
|
||||
if (remoteFolder == null)
|
||||
{
|
||||
// Local folder doesn't exists remotely. Delete local copy.
|
||||
await _gmailChangeProcessor.DeleteFolderAsync(Account.Id, localFolder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
deletedFolders.Add(localFolder);
|
||||
}
|
||||
// Insert new calendar.
|
||||
var localCalendar = calendar.AsCalendar(Account.Id);
|
||||
insertedCalendars.Add(localCalendar);
|
||||
}
|
||||
|
||||
// Delete the deleted folders from local list.
|
||||
deletedFolders.ForEach(a => localFolders.Remove(a));
|
||||
|
||||
// 2. Handle update/insert based on remote folders.
|
||||
foreach (var remoteFolder in labelsResponse.Labels)
|
||||
else
|
||||
{
|
||||
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.Id);
|
||||
|
||||
if (existingLocalFolder == null)
|
||||
// Update existing calendar. Right now we only update the name.
|
||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar))
|
||||
{
|
||||
// Insert new folder.
|
||||
var localFolder = remoteFolder.GetLocalFolder(labelsResponse, Account.Id);
|
||||
existingLocalCalendar.Name = calendar.Summary;
|
||||
|
||||
insertedFolders.Add(localFolder);
|
||||
updatedCalendars.Add(existingLocalCalendar);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing folder. Right now we only update the name.
|
||||
|
||||
// TODO: Moving folders around different parents. This is not supported right now.
|
||||
// We will need more comphrensive folder update mechanism to support this.
|
||||
|
||||
if (ShouldUpdateFolder(remoteFolder, existingLocalFolder))
|
||||
{
|
||||
existingLocalFolder.FolderName = remoteFolder.Name;
|
||||
existingLocalFolder.TextColorHex = remoteFolder.Color?.TextColor;
|
||||
existingLocalFolder.BackgroundColorHex = remoteFolder.Color?.BackgroundColor;
|
||||
|
||||
updatedFolders.Add(existingLocalFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove it from the local folder list to skip additional folder updates.
|
||||
localFolders.Remove(existingLocalFolder);
|
||||
}
|
||||
// Remove it from the local folder list to skip additional calendar updates.
|
||||
localCalendars.Remove(existingLocalCalendar);
|
||||
}
|
||||
}
|
||||
|
||||
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
|
||||
foreach (var folder in insertedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in updatedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (insertedFolders.Any() || deletedFolders.Any() || updatedFolders.Any())
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
foreach (var calendar in insertedCalendars)
|
||||
{
|
||||
throw;
|
||||
await _gmailChangeProcessor.InsertAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var calendar in updatedCalendars)
|
||||
{
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (insertedCalendars.Any() || deletedCalendars.Any() || updatedCalendars.Any())
|
||||
{
|
||||
// TODO: Notify calendar updates.
|
||||
// WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var folderRequest = _gmailService.Users.Labels.List("me");
|
||||
|
||||
var labelsResponse = await folderRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (labelsResponse.Labels == null)
|
||||
{
|
||||
_logger.Warning("No folders found for {Name}", Account.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
List<MailItemFolder> insertedFolders = new();
|
||||
List<MailItemFolder> updatedFolders = new();
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
// 1. Handle deleted labels.
|
||||
|
||||
foreach (var localFolder in localFolders)
|
||||
{
|
||||
// Category folder is virtual folder for Wino. Skip it.
|
||||
if (localFolder.SpecialFolderType == SpecialFolderType.Category) continue;
|
||||
|
||||
var remoteFolder = labelsResponse.Labels.FirstOrDefault(a => a.Id == localFolder.RemoteFolderId);
|
||||
|
||||
if (remoteFolder == null)
|
||||
{
|
||||
// Local folder doesn't exists remotely. Delete local copy.
|
||||
await _gmailChangeProcessor.DeleteFolderAsync(Account.Id, localFolder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
deletedFolders.Add(localFolder);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the deleted folders from local list.
|
||||
deletedFolders.ForEach(a => localFolders.Remove(a));
|
||||
|
||||
// 2. Handle update/insert based on remote folders.
|
||||
foreach (var remoteFolder in labelsResponse.Labels)
|
||||
{
|
||||
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.Id);
|
||||
|
||||
if (existingLocalFolder == null)
|
||||
{
|
||||
// Insert new folder.
|
||||
var localFolder = remoteFolder.GetLocalFolder(labelsResponse, Account.Id);
|
||||
|
||||
insertedFolders.Add(localFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing folder. Right now we only update the name.
|
||||
|
||||
// TODO: Moving folders around different parents. This is not supported right now.
|
||||
// We will need more comphrensive folder update mechanism to support this.
|
||||
|
||||
if (ShouldUpdateFolder(remoteFolder, existingLocalFolder))
|
||||
{
|
||||
existingLocalFolder.FolderName = remoteFolder.Name;
|
||||
existingLocalFolder.TextColorHex = remoteFolder.Color?.TextColor;
|
||||
existingLocalFolder.BackgroundColorHex = remoteFolder.Color?.BackgroundColor;
|
||||
|
||||
updatedFolders.Add(existingLocalFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove it from the local folder list to skip additional folder updates.
|
||||
localFolders.Remove(existingLocalFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
|
||||
foreach (var folder in insertedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in updatedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (insertedFolders.Any() || deletedFolders.Any() || updatedFolders.Any())
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUpdateCalendar(CalendarListEntry calendarListEntry, AccountCalendar accountCalendar)
|
||||
{
|
||||
// TODO: Only calendar name is updated for now. We can add more checks here.
|
||||
|
||||
var remoteCalendarName = calendarListEntry.Summary;
|
||||
var localCalendarName = accountCalendar.Name;
|
||||
|
||||
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool ShouldUpdateFolder(Label remoteFolder, MailItemFolder existingLocalFolder)
|
||||
|
||||
@@ -257,6 +257,18 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Execute requests for calendar events.
|
||||
return SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates unread item counts for some folders and account.
|
||||
/// Sends a message that shell can pick up and update the UI.
|
||||
@@ -355,5 +367,7 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<PackageReference Include="MimeKit" Version="4.9.0" />
|
||||
<PackageReference Include="morelinq" Version="4.3.0" />
|
||||
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.0" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||
|
||||
@@ -29,7 +29,7 @@ using Wino.Services;
|
||||
|
||||
namespace Wino
|
||||
{
|
||||
public sealed partial class App : WinoApplication, IRecipient<NewSynchronizationRequested>
|
||||
public sealed partial class App : WinoApplication, IRecipient<NewMailSynchronizationRequested>
|
||||
{
|
||||
public override string AppCenterKey { get; } = "90deb1d0-a77f-47d0-8a6b-7eaf111c6b72";
|
||||
|
||||
@@ -224,11 +224,11 @@ namespace Wino
|
||||
AppServiceConnectionManager.Connection = null;
|
||||
}
|
||||
|
||||
public async void Receive(NewSynchronizationRequested message)
|
||||
public async void Receive(NewMailSynchronizationRequested message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<MailSynchronizationResult, NewSynchronizationRequested>(message);
|
||||
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(message);
|
||||
synchronizationResultResponse.ThrowIfFailed();
|
||||
}
|
||||
catch (WinoServerException serverException)
|
||||
|
||||
10
Wino.Messages/Client/Calendar/CalendarEventMessages.cs
Normal file
10
Wino.Messages/Client/Calendar/CalendarEventMessages.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
namespace Wino.Messaging.Client.Calendar
|
||||
{
|
||||
public record CalendarListRefreshed(List<AccountCalendar> AccountCalendars);
|
||||
public record CalendarListAdded(AccountCalendar AccountCalendar);
|
||||
public record CalendarListUpdated(AccountCalendar AccountCalendar);
|
||||
public record CalendarListDeleted(AccountCalendar AccountCalendar);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
|
||||
namespace Wino.Messaging.Client.Calendar
|
||||
{
|
||||
public record CalendarItemAdded(CalendarItem CalendarItem);
|
||||
public record CalendarItemUpdated(CalendarItem CalendarItem);
|
||||
public record CalendarItemDeleted(CalendarItem CalendarItem);
|
||||
}
|
||||
@@ -5,8 +5,14 @@ using Wino.Core.Domain.Models.Synchronization;
|
||||
namespace Wino.Messaging.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Triggers a new synchronization if possible.
|
||||
/// Triggers a new mail synchronization if possible.
|
||||
/// </summary>
|
||||
/// <param name="Options">Options for synchronization.</param>
|
||||
public record NewSynchronizationRequested(MailSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage;
|
||||
public record NewMailSynchronizationRequested(MailSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a new calendar synchronization if possible.
|
||||
/// </summary>
|
||||
/// <param name="Options">Options for synchronization.</param>
|
||||
public record NewCalendarSynchronizationRequested(CalendarSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace Wino.Server.Core
|
||||
{
|
||||
return typeName switch
|
||||
{
|
||||
nameof(NewSynchronizationRequested) => App.Current.Services.GetService<SynchronizationRequestHandler>(),
|
||||
nameof(NewMailSynchronizationRequested) => App.Current.Services.GetService<MailSynchronizationRequestHandler>(),
|
||||
nameof(NewCalendarSynchronizationRequested) => App.Current.Services.GetService<CalendarSynchronizationRequestHandler>(),
|
||||
nameof(ServerRequestPackage) => App.Current.Services.GetService<UserActionRequestHandler>(),
|
||||
nameof(DownloadMissingMessageRequested) => App.Current.Services.GetService<SingleMimeDownloadHandler>(),
|
||||
nameof(AuthorizationRequested) => App.Current.Services.GetService<AuthenticationHandler>(),
|
||||
@@ -31,7 +32,8 @@ namespace Wino.Server.Core
|
||||
{
|
||||
// Register all known handlers.
|
||||
|
||||
serviceCollection.AddTransient<SynchronizationRequestHandler>();
|
||||
serviceCollection.AddTransient<MailSynchronizationRequestHandler>();
|
||||
serviceCollection.AddTransient<CalendarSynchronizationRequestHandler>();
|
||||
serviceCollection.AddTransient<UserActionRequestHandler>();
|
||||
serviceCollection.AddTransient<SingleMimeDownloadHandler>();
|
||||
serviceCollection.AddTransient<AuthenticationHandler>();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Server;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Server.Core;
|
||||
|
||||
namespace Wino.Server.MessageHandlers
|
||||
{
|
||||
public class CalendarSynchronizationRequestHandler : ServerMessageHandler<NewCalendarSynchronizationRequested, CalendarSynchronizationResult>
|
||||
{
|
||||
public override WinoServerResponse<CalendarSynchronizationResult> FailureDefaultResponse(Exception ex)
|
||||
=> WinoServerResponse<CalendarSynchronizationResult>.CreateErrorResponse(ex.Message);
|
||||
|
||||
private readonly ISynchronizerFactory _synchronizerFactory;
|
||||
|
||||
public CalendarSynchronizationRequestHandler(ISynchronizerFactory synchronizerFactory)
|
||||
{
|
||||
_synchronizerFactory = synchronizerFactory;
|
||||
}
|
||||
|
||||
protected override async Task<WinoServerResponse<CalendarSynchronizationResult>> HandleAsync(NewCalendarSynchronizationRequested message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(message.Options.AccountId);
|
||||
|
||||
try
|
||||
{
|
||||
var synchronizationResult = await synchronizer.SynchronizeCalendarEventsAsync(message.Options, cancellationToken);
|
||||
|
||||
return WinoServerResponse<CalendarSynchronizationResult>.CreateSuccessResponse(synchronizationResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ using Wino.Server.Core;
|
||||
namespace Wino.Server.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for NewSynchronizationRequested from the client.
|
||||
/// Handler for NewMailSynchronizationRequested from the client.
|
||||
/// </summary>
|
||||
public class SynchronizationRequestHandler : ServerMessageHandler<NewSynchronizationRequested, MailSynchronizationResult>
|
||||
public class MailSynchronizationRequestHandler : ServerMessageHandler<NewMailSynchronizationRequested, MailSynchronizationResult>
|
||||
{
|
||||
public override WinoServerResponse<MailSynchronizationResult> FailureDefaultResponse(Exception ex)
|
||||
=> WinoServerResponse<MailSynchronizationResult>.CreateErrorResponse(ex.Message);
|
||||
@@ -25,7 +25,7 @@ namespace Wino.Server.MessageHandlers
|
||||
private readonly INotificationBuilder _notificationBuilder;
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public SynchronizationRequestHandler(ISynchronizerFactory synchronizerFactory,
|
||||
public MailSynchronizationRequestHandler(ISynchronizerFactory synchronizerFactory,
|
||||
INotificationBuilder notificationBuilder,
|
||||
IFolderService folderService)
|
||||
{
|
||||
@@ -34,7 +34,7 @@ namespace Wino.Server.MessageHandlers
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
protected override async Task<WinoServerResponse<MailSynchronizationResult>> HandleAsync(NewSynchronizationRequested message, CancellationToken cancellationToken = default)
|
||||
protected override async Task<WinoServerResponse<MailSynchronizationResult>> HandleAsync(NewMailSynchronizationRequested message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(message.Options.AccountId);
|
||||
|
||||
@@ -84,6 +84,10 @@ namespace Wino.Server
|
||||
|
||||
private async void SynchronizationTimerTriggered(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
// TODO: Disabled for calendar synchronization. Implement a separate timer for calendar synchronization.
|
||||
// or completely separate contexts for both apps.
|
||||
return;
|
||||
|
||||
// Send sync request for all accounts.
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
@@ -96,7 +100,7 @@ namespace Wino.Server
|
||||
Type = MailSynchronizationType.InboxOnly,
|
||||
};
|
||||
|
||||
var request = new NewSynchronizationRequested(options, SynchronizationSource.Server);
|
||||
var request = new NewMailSynchronizationRequested(options, SynchronizationSource.Server);
|
||||
|
||||
await ExecuteServerMessageSafeAsync(null, request);
|
||||
}
|
||||
@@ -279,10 +283,15 @@ namespace Wino.Server
|
||||
{
|
||||
switch (typeName)
|
||||
{
|
||||
case nameof(NewSynchronizationRequested):
|
||||
Debug.WriteLine($"New synchronization requested.");
|
||||
case nameof(NewMailSynchronizationRequested):
|
||||
Debug.WriteLine($"New mail synchronization requested.");
|
||||
|
||||
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<NewSynchronizationRequested>(messageJson, _jsonSerializerOptions));
|
||||
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<NewMailSynchronizationRequested>(messageJson, _jsonSerializerOptions));
|
||||
break;
|
||||
case nameof(NewCalendarSynchronizationRequested):
|
||||
Debug.WriteLine($"New calendar synchronization requested.");
|
||||
|
||||
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<NewCalendarSynchronizationRequested>(messageJson, _jsonSerializerOptions));
|
||||
break;
|
||||
case nameof(DownloadMissingMessageRequested):
|
||||
Debug.WriteLine($"Download missing message requested.");
|
||||
|
||||
71
Wino.Services/CalendarService.cs
Normal file
71
Wino.Services/CalendarService.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services
|
||||
{
|
||||
public class CalendarService : BaseDatabaseService, ICalendarService
|
||||
{
|
||||
public CalendarService(IDatabaseService databaseService) : base(databaseService)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
||||
=> Connection.Table<AccountCalendar>().Where(x => x.AccountId == accountId).ToListAsync();
|
||||
|
||||
public async Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
await Connection.InsertAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListAdded(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
await Connection.UpdateAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListUpdated(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
var deleteCalendarItemsQuery = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.CalendarId), accountCalendar.Id)
|
||||
.Where(nameof(AccountCalendar.AccountId), accountCalendar.AccountId);
|
||||
|
||||
var rawQuery = deleteCalendarItemsQuery.GetRawQuery();
|
||||
|
||||
await Connection.ExecuteAsync(rawQuery);
|
||||
await Connection.DeleteAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListDeleted(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
{
|
||||
var calendarItem = await Connection.GetAsync<CalendarItem>(calendarItemId);
|
||||
|
||||
if (calendarItem == null) return;
|
||||
|
||||
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == calendarItemId);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(calendarItem));
|
||||
}
|
||||
|
||||
public Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
|
||||
{
|
||||
return Connection.RunInTransactionAsync((conn) =>
|
||||
{
|
||||
conn.Insert(calendarItem);
|
||||
conn.InsertAll(attendees);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,15 @@ namespace Wino.Services
|
||||
{
|
||||
public ContactService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
|
||||
public async Task<AccountContact> CreateNewContactAsync(string address, string displayName)
|
||||
{
|
||||
var contact = new AccountContact() { Address = address, Name = displayName };
|
||||
|
||||
await Connection.InsertAsync(contact).ConfigureAwait(false);
|
||||
|
||||
return contact;
|
||||
}
|
||||
|
||||
public Task<List<AccountContact>> GetAddressInformationAsync(string queryText)
|
||||
{
|
||||
if (queryText == null || queryText.Length < 2)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
@@ -52,7 +53,11 @@ namespace Wino.Services
|
||||
typeof(AccountSignature),
|
||||
typeof(MergedInbox),
|
||||
typeof(MailAccountPreferences),
|
||||
typeof(MailAccountAlias)
|
||||
typeof(MailAccountAlias),
|
||||
typeof(AccountCalendar),
|
||||
typeof(CalendarEventAttendee),
|
||||
typeof(CalendarItem),
|
||||
typeof(Reminder)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Wino.Services
|
||||
services.AddSingleton<ILaunchProtocolService, LaunchProtocolService>();
|
||||
services.AddSingleton<IMimeFileService, MimeFileService>();
|
||||
|
||||
services.AddTransient<ICalendarService, CalendarService>();
|
||||
services.AddTransient<IMailService, MailService>();
|
||||
services.AddTransient<IFolderService, FolderService>();
|
||||
services.AddTransient<IAccountService, AccountService>();
|
||||
|
||||
Reference in New Issue
Block a user