3 Commits

Author SHA1 Message Date
Burak Kaan Köse
6db85bfa39 Some event selections 2025-07-07 21:13:53 +02:00
Burak Kaan Köse
03c9ac1e11 Local time rendering for events. 2025-07-07 21:03:07 +02:00
Burak Kaan Köse
1ee0063b62 Refactored the calendar synchronization code using AI. 2025-07-06 17:25:38 +02:00
33 changed files with 3215 additions and 1090 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
@@ -141,6 +142,16 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
Type = CalendarSynchronizationType.CalendarMetadata
};
var timer = new Stopwatch();
var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
timer.Stop();
Debug.WriteLine("Synchronization completed in {timer.ElapsedMilliseconds} ms");
// TODO: Properly handle synchronization errors.
accountCreationDialog.Complete(false);
}
}

View File

@@ -127,6 +127,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
private readonly IKeyPressService _keyPressService;
private readonly ICalendarServiceEx _calendarServiceEx;
private readonly IPreferencesService _preferencesService;
// Store latest rendered options.
@@ -146,6 +147,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
ICalendarService calendarService,
INavigationService navigationService,
IKeyPressService keyPressService,
ICalendarServiceEx calendarServiceEx,
IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService)
{
@@ -155,6 +157,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
_calendarService = calendarService;
_navigationService = navigationService;
_keyPressService = keyPressService;
_calendarServiceEx = calendarServiceEx;
_preferencesService = preferencesService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
@@ -235,23 +238,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
private async Task SaveQuickEventAsync()
{
var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds;
var testCalendarItem = new CalendarItem
{
CalendarId = SelectedQuickEventAccountCalendar.Id,
StartDate = QuickEventStartTime,
DurationInSeconds = durationSeconds,
CreatedAt = DateTime.UtcNow,
Description = string.Empty,
Location = Location,
Title = EventName,
Id = Guid.NewGuid()
};
IsQuickEventDialogOpen = false;
await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null);
// TODO: Create the request with the synchronizer.
}
@@ -635,10 +621,13 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Check all the events for the given date range and calendar.
// Then find the day representation for all the events returned, and add to the collection.
var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);
var events = await _calendarServiceEx.GetExpandedEventsInDateRangeWithExceptionsAsync(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End, calendarViewModel.AccountCalendar).ConfigureAwait(false);
foreach (var @event in events)
{
// TODO: Do it in the service.
@event.AssignedCalendar = calendarViewModel.AccountCalendar;
// Find the days that the event falls into.
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
@@ -800,12 +789,12 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Recurring events must be selected as a single instance.
// We need to find the day that the event is in, and then select the event.
if (!calendarItemViewModel.IsRecurringEvent)
{
return [calendarItemViewModel];
}
else
{
if (calendarItemViewModel == null) return Enumerable.Empty<CalendarItemViewModel>();
// If the calendar item is not recurring, we can just return it.
if (calendarItemViewModel.ItemType == CalendarItemType.Timed || calendarItemViewModel.ItemType == CalendarItemType.RecurringException) return new[] { calendarItemViewModel };
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
@@ -813,7 +802,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
.Cast<CalendarItemViewModel>()
.Distinct();
}
}
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
@@ -821,6 +809,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
if (itemsToUnselect == null) return;
foreach (var item in itemsToUnselect)
{
item.IsSelected = false;
@@ -833,6 +823,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
if (itemsToSelect == null) return;
foreach (var item in itemsToSelect)
{
item.IsSelected = true;

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data;
@@ -17,28 +19,73 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
public DateTime StartDateTime { get => CalendarItem.StartDateTime; set => CalendarItem.StartDateTime = value; }
public DateTime EndDate => CalendarItem.EndDate;
public DateTime EndDateTime => CalendarItem.EndDateTime;
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
/// <summary>
/// Gets the start date and time in the local time zone for display purposes.
/// </summary>
public DateTime LocalStartDateTime => ConvertToLocalTime();
/// <summary>
/// Gets the end date and time in the local time zone for display purposes.
/// </summary>
public DateTime LocalEndDateTime => ConvertToLocalTime();
public ITimePeriod Period => CalendarItem.Period;
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.RecurrenceRules) || !string.IsNullOrEmpty(CalendarItem.RecurringEventId);
[ObservableProperty]
private bool _isSelected;
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemType ItemType => CalendarItem.ItemType;
public CalendarItemViewModel(CalendarItem calendarItem)
{
CalendarItem = calendarItem;
Debug.WriteLine($"{Title} : {ItemType}");
}
/// <summary>
/// Converts a DateTime to local time based on the provided timezone.
/// If timezone is empty or null, assumes the DateTime is in UTC.
/// </summary>
/// <param name="dateTime">The DateTime to convert</param>
/// <param name="timeZone">The timezone string. If empty/null, assumes UTC.</param>
/// <returns>DateTime converted to local time</returns>
private DateTime ConvertToLocalTime()
{
// All day events ignore time zones and are treated as local time.
if (ItemType == CalendarItemType.AllDay || ItemType == CalendarItemType.MultiDayAllDay || ItemType == CalendarItemType.RecurringAllDay)
return CalendarItem.StartDateTime;
if (string.IsNullOrEmpty(CalendarItem.TimeZone))
{
// If no timezone specified, assume it's UTC and convert to local time
return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime();
}
try
{
// Parse the timezone and convert to local time
var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.TimeZone);
return TimeZoneInfo.ConvertTimeToUtc(CalendarItem.StartDateTime, sourceTimeZone).ToLocalTime();
}
catch (TimeZoneNotFoundException)
{
// If timezone is not found, fallback to treating as UTC
return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime();
}
catch (InvalidTimeZoneException)
{
// If timezone is invalid, fallback to treating as UTC
return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime();
}
}
public override string ToString() => CalendarItem.Title;

View File

@@ -31,7 +31,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
[ObservableProperty]
private CalendarItemViewModel _seriesParent;
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
public bool CanViewSeries => false; //CurrentEvent?.IsRecurringChild ?? false; // TODO: Implement this properly
#endregion
@@ -67,7 +67,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
CurrentEvent = new CalendarItemViewModel(currentEventItem);
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId);
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.Id);
foreach (var item in attendees)
{

View File

@@ -69,7 +69,7 @@
VerticalAlignment="Top"
Orientation="Horizontal"
Spacing="6">
<controls:WinoFontIcon
<!--<controls:WinoFontIcon
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventRepeat"
@@ -79,7 +79,7 @@
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventMuiltiDay"
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />-->
</StackPanel>
<VisualStateManager.VisualStateGroups>

View File

@@ -7,6 +7,7 @@ using Windows.UI.Xaml.Input;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
@@ -90,7 +91,9 @@ public sealed partial class CalendarItemControl : UserControl
if (CalendarItem == null) return;
if (DisplayingDate == null) return;
if (CalendarItem.IsMultiDayEvent)
bool isMultiDayEvent = CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDay || CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay;
if (isMultiDayEvent)
{
// Multi day events are divided into 3 categories:
// 1. All day events
@@ -103,14 +106,14 @@ public sealed partial class CalendarItemControl : UserControl
periodRelation == PeriodRelation.EnclosingStartTouching)
{
// hour -> title
CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}";
CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDateTime.TimeOfDay)} -> {CalendarItem.Title}";
}
else if (
periodRelation == PeriodRelation.EndInside ||
periodRelation == PeriodRelation.EnclosingEndTouching)
{
// title <- hour
CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}";
CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDateTime.TimeOfDay)}";
}
else if (periodRelation == PeriodRelation.Enclosing)
{
@@ -139,11 +142,11 @@ public sealed partial class CalendarItemControl : UserControl
{
if (CalendarItem == null) return;
if (CalendarItem.IsAllDayEvent)
if (CalendarItem.CalendarItem.ItemType == CalendarItemType.AllDay)
{
VisualStateManager.GoToState(this, "AllDayEvent", true);
}
else if (CalendarItem.IsMultiDayEvent)
else if (CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay || CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDay)
{
if (IsCustomEventArea)
{

View File

@@ -9,6 +9,7 @@ using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Calendar.Models;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.Controls;
@@ -44,9 +45,9 @@ public partial class WinoCalendarPanel : Panel
private void ResetMeasurements() => _measurements.Clear();
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
private double GetChildTopMargin(ICalendarItemViewModel calendarItemViewModel, double availableHeight)
{
var childStart = calendarItemViewModel.StartDate;
var childStart = calendarItemViewModel.LocalStartDateTime;
if (childStart <= Period.Start)
{
@@ -68,10 +69,10 @@ public partial class WinoCalendarPanel : Panel
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
=> availableWidth * calendarItemMeasurement.Left;
private double GetChildHeight(ICalendarItem child)
private double GetChildHeight(ICalendarItemViewModel child)
{
// All day events are not measured.
if (child.IsAllDayEvent) return 0;
if (child.ItemType == CalendarItemType.AllDay) return 0;
double childDurationInMinutes = 0d;
double availableHeight = HourHeight * 24;
@@ -80,7 +81,7 @@ public partial class WinoCalendarPanel : Panel
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
if (!child.IsMultiDayEvent)
if (child.ItemType != CalendarItemType.MultiDay || child.ItemType != CalendarItemType.MultiDayAllDay)
{
childDurationInMinutes = child.Period.Duration.TotalMinutes;
}
@@ -127,7 +128,7 @@ public partial class WinoCalendarPanel : Panel
foreach (var control in calendarControls)
{
// We can't arrange this child.
if (!(control.Content is ICalendarItem child)) continue;
if (!(control.Content is ICalendarItemViewModel child)) continue;
bool isHorizontallyLastItem = false;
@@ -160,7 +161,7 @@ public partial class WinoCalendarPanel : Panel
double extraRightMargin = 0;
// Multi-day events don't have any margin and their hit test is disabled.
if (!child.IsMultiDayEvent)
if (child.ItemType != CalendarItemType.MultiDay || child.ItemType != CalendarItemType.MultiDayAllDay)
{
// Max of 5% of the width or 20px max.
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
@@ -168,8 +169,10 @@ public partial class WinoCalendarPanel : Panel
if (childWidth < 0) childWidth = 1;
bool isAllOrMultiDayEvent = child.ItemType == CalendarItemType.AllDay || child.ItemType == CalendarItemType.MultiDay || child.ItemType == CalendarItemType.MultiDayAllDay;
// Regular events must have 2px margin
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
if (!isAllOrMultiDayEvent)
{
childLeft += 2;
childTop += 2;
@@ -211,10 +214,12 @@ public partial class WinoCalendarPanel : Panel
var columns = new List<List<ICalendarItem>>();
DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
foreach (var ev in events.OrderBy(ev => ev.StartDateTime).ThenBy(ev => ev.EndDateTime))
{
// Multi-day events are not measured.
if (ev.IsMultiDayEvent) continue;
bool isMultiDayEvent = ev.ItemType == CalendarItemType.MultiDay || ev.ItemType == CalendarItemType.MultiDayAllDay;
if (isMultiDayEvent) continue;
if (ev.Period.Start >= lastEventEnding)
{

View File

@@ -22,6 +22,7 @@ public static class CalendarXamlHelpers
/// </summary>
public static string GetEventDetailsDateString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
// TODO: This is not correct.
if (calendarItemViewModel == null || settings == null) return string.Empty;
var start = calendarItemViewModel.Period.Start;
@@ -30,7 +31,7 @@ public static class CalendarXamlHelpers
string timeFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "h:mm tt" : "HH:mm";
string dateFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "dddd, dd MMMM h:mm tt" : "dddd, dd MMMM HH:mm";
if (calendarItemViewModel.IsMultiDayEvent)
if (calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDay || calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay)
{
return $"{start.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)} - {end.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)}";
}
@@ -42,54 +43,14 @@ public static class CalendarXamlHelpers
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
{
if (calendarItemViewModel == null || !calendarItemViewModel.IsRecurringChild) return string.Empty;
// Parse recurrence rules
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(calendarItemViewModel.StartDate),
End = new CalDateTime(calendarItemViewModel.EndDate),
};
var recurrenceLines = Regex.Split(calendarItemViewModel.CalendarItem.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
if (calendarEvent.RecurrenceRules == null || !calendarEvent.RecurrenceRules.Any())
{
return "No recurrence pattern.";
}
var recurrenceRule = calendarEvent.RecurrenceRules.First();
var daysOfWeek = string.Join(", ", recurrenceRule.ByDay.Select(day => day.DayOfWeek.ToString()));
string timeZone = calendarEvent.DtStart.TzId ?? "UTC";
return $"Every {daysOfWeek}, effective {calendarEvent.DtStart.Value.ToShortDateString()} " +
$"from {calendarEvent.DtStart.Value.ToShortTimeString()} to {calendarEvent.DtEnd.Value.ToShortTimeString()} " +
$"{timeZone}.";
// TODO
return string.Empty;
}
public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || settings == null) return string.Empty;
// Single event in a day.
if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}";
}
else if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}";
}
else
{
// All day event.
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})";
}
// TODO
return string.Empty;
}
public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
@@ -98,8 +59,12 @@ public static class CalendarXamlHelpers
{
if (calendarItemViewModel == null) return PopupPlacementMode.Auto;
bool isAllDayOrMultiDay = calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDay ||
calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.AllDay ||
calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay;
// All and/or multi day events always go to the top of the screen.
if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom;
if (isAllDayOrMultiDay) return PopupPlacementMode.Bottom;
return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType);
}

View File

@@ -13,7 +13,8 @@ public partial class CustomAreaCalendarItemSelector : DataTemplateSelector
{
if (item is CalendarItemViewModel calendarItemViewModel)
{
return calendarItemViewModel.IsMultiDayEvent ? MultiDayTemplate : AllDayTemplate;
return calendarItemViewModel.CalendarItem.ItemType == Core.Domain.Enums.CalendarItemType.MultiDay ||
calendarItemViewModel.CalendarItem.ItemType == Core.Domain.Enums.CalendarItemType.MultiDayAllDay ? MultiDayTemplate : AllDayTemplate;
}
return base.SelectTemplateCore(item, container);

View File

@@ -89,6 +89,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
BorderBrush="Transparent"
IsTabStop="False"
PlaceholderText="Search" />
<StackPanel

View File

@@ -256,7 +256,7 @@
<PersonPicture
Width="40"
Height="40"
DisplayName="{x:Bind Name}" />
DisplayName="{x:Bind DisplayName}" />
<!-- TODO: Organizer -->
<Grid Grid.Column="1">
@@ -265,7 +265,7 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind DisplayName}" />
<TextBlock
Grid.Row="1"
FontSize="13"

View File

@@ -80,11 +80,11 @@ public class CalendarEventCollection
// Multi-day events go to both.
// Anything else goes to regular.
if (calendarItem.IsAllDayEvent)
if (calendarItem.ItemType == Enums.CalendarItemType.AllDay || calendarItem.ItemType == Enums.CalendarItemType.MultiDayAllDay || calendarItem.ItemType == Enums.CalendarItemType.RecurringAllDay)
{
return [_internalAllDayEvents];
}
else if (calendarItem.IsMultiDayEvent)
else if (calendarItem.ItemType == Enums.CalendarItemType.MultiDay || calendarItem.ItemType == Enums.CalendarItemType.MultiDayAllDay)
{
return [_internalRegularEvents, _internalAllDayEvents];
}

View File

@@ -8,8 +8,6 @@ public static class Constants
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
public const string LocalDraftStartPrefix = "localDraft_";
public const string CalendarEventRecurrenceRuleSeperator = "___";
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey);

View File

@@ -7,12 +7,40 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class AccountCalendar : IAccountCalendar
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
[NotNull]
public string RemoteCalendarId { get; set; } = string.Empty;
[NotNull]
public Guid AccountId { get; set; }
public string RemoteCalendarId { get; set; }
public string SynchronizationDeltaToken { get; set; }
public string Name { get; set; }
public bool IsPrimary { get; set; }
[NotNull]
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Location { get; set; }
public string? TimeZone { get; set; }
public string? AccessRole { get; set; }
public bool IsPrimary { get; set; } = false;
public string? BackgroundColor { get; set; }
public string? ForegroundColor { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
public DateTime? LastSyncTime { get; set; }
public string? SynchronizationDeltaToken { get; set; }
public bool IsDeleted { get; set; } = false;
public bool IsExtended { get; set; } = true;
/// <summary>
@@ -20,5 +48,4 @@ public class AccountCalendar : IAccountCalendar
/// </summary>
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; }
}

View File

@@ -8,12 +8,29 @@ namespace Wino.Core.Domain.Entities.Calendar;
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; }
public Guid Id { get; set; } = Guid.NewGuid();
[NotNull]
public Guid EventId { get; set; }
[NotNull]
public string Email { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public AttendeeResponseStatus ResponseStatus { get; set; } = AttendeeResponseStatus.NeedsAction;
public bool IsOptional { get; set; } = false;
public bool IsOrganizer { get; set; } = false;
public bool IsSelf { get; set; } = false;
public string? Comment { get; set; }
public int? AdditionalGuests { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
}

View File

@@ -3,6 +3,7 @@ using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Helpers;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
@@ -11,169 +12,81 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class CalendarItem : ICalendarItem
{
[PrimaryKey]
public Guid Id { get; set; }
public string RemoteEventId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime StartDate { get; set; }
[NotNull]
public string RemoteEventId { get; set; } = string.Empty;
public DateTime EndDate
{
get
{
return StartDate.AddSeconds(DurationInSeconds);
}
}
[NotNull]
public Guid CalendarId { get; set; }
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
[Ignore]
public IAccountCalendar AssignedCalendar { get; set; }
[NotNull]
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Location { get; set; }
public string? HtmlLink { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
{
get
{
_period ??= new TimeRange(StartDate, EndDate);
_period ??= new TimeRange(StartDateTime, EndDateTime);
return _period;
}
}
/// <summary>
/// Events that starts at midnight and ends at midnight are considered all-day events.
/// </summary>
public bool IsAllDayEvent
{
get
{
return
StartDate.TimeOfDay == TimeSpan.Zero &&
EndDate.TimeOfDay == TimeSpan.Zero;
}
}
public bool IsAllDay { get; set; }
public string? TimeZone { get; set; }
public string? RecurrenceRules { get; set; }
public string? Status { get; set; }
public string? OrganizerDisplayName { get; set; }
public string? OrganizerEmail { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
public bool IsDeleted { get; set; }
public string? RecurringEventId { get; set; }
public string? OriginalStartTime { get; set; }
/// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences.
/// IsOccurrence is used to display occurrence instances of parent recurring events.
/// IsOccurrence == false && IsRecurringChild == true => exceptional single instance.
/// The type of calendar item (Timed, AllDay, MultiDay, etc.)
/// </summary>
public bool IsRecurringChild
{
get
{
return RecurringCalendarItemId != null;
}
}
public CalendarItemType ItemType { get; set; }
/// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences.
/// Automatically determines and sets the ItemType based on event properties
/// </summary>
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
public void DetermineItemType()
{
var hasRecurrence = !string.IsNullOrEmpty(RecurrenceRules);
var isCancelled = Status?.ToLowerInvariant() == "cancelled" || IsDeleted;
/// <summary>
/// Events that are the master event definition of recurrence events.
/// </summary>
public bool IsRecurringParent
{
get
{
return !string.IsNullOrEmpty(Recurrence) && RecurringCalendarItemId == null;
}
}
/// <summary>
/// Events that are not all-day events and last more than one day are considered multi-day events.
/// </summary>
public bool IsMultiDayEvent
{
get
{
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent;
}
}
public double DurationInSeconds { get; set; }
public string Recurrence { get; set; }
public string OrganizerDisplayName { get; set; }
public string OrganizerEmail { get; set; }
/// <summary>
/// The id of the parent calendar item of the recurring event.
/// Exceptional instances are stored as a separate calendar item.
/// This makes the calendar item a child of the recurring event.
/// </summary>
public Guid? RecurringCalendarItemId { get; set; }
/// <summary>
/// Indicates read-only events. Default is false.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// Hidden events must not be displayed to the user.
/// This usually happens when a child instance of recurring parent is cancelled after creation.
/// </summary>
public bool IsHidden { get; set; }
// TODO
public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; }
public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; }
[Ignore]
public IAccountCalendar AssignedCalendar { get; set; }
/// <summary>
/// Whether this item does not really exist in the database or not.
/// These are used to display occurrence instances of parent recurring events.
/// </summary>
[Ignore]
public bool IsOccurrence { get; set; }
/// <summary>
/// Id to load information related to this event.
/// Occurrences tracked by the parent recurring event if they are not exceptional instances.
/// Recurring children here are exceptional instances. They have their own info in the database including Id.
/// </summary>
public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id;
public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds)
{
// Create a copy with the new start date and duration
return new CalendarItem
{
Id = Guid.NewGuid(),
Title = Title,
Description = Description,
Location = Location,
StartDate = startDate,
DurationInSeconds = durationInSeconds,
Recurrence = Recurrence,
OrganizerDisplayName = OrganizerDisplayName,
OrganizerEmail = OrganizerEmail,
RecurringCalendarItemId = Id,
AssignedCalendar = AssignedCalendar,
CalendarId = CalendarId,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
Visibility = Visibility,
Status = Status,
CustomEventColorHex = CustomEventColorHex,
HtmlLink = HtmlLink,
StartDateOffset = StartDateOffset,
EndDateOffset = EndDateOffset,
RemoteEventId = RemoteEventId,
IsHidden = IsHidden,
IsLocked = IsLocked,
IsOccurrence = true
};
ItemType = CalendarItemTypeHelper.DetermineItemType(
StartDateTime,
EndDateTime,
IsAllDay,
hasRecurrence,
isCancelled,
Status);
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Represents the response status of an attendee to a calendar event
/// </summary>
public enum AttendeeResponseStatus
{
/// <summary>
/// The attendee has not responded to the invitation
/// </summary>
NeedsAction = 0,
/// <summary>
/// The attendee has accepted the invitation
/// </summary>
Accepted = 1,
/// <summary>
/// The attendee has declined the invitation
/// </summary>
Declined = 2,
/// <summary>
/// The attendee has tentatively accepted the invitation
/// </summary>
Tentative = 3
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Enums;
public enum CalendarItemType
{
/// <summary>
/// A standard timed event with specific start and end times on the same day
/// </summary>
Timed = 0,
/// <summary>
/// An all-day event that spans exactly one day
/// </summary>
AllDay = 1,
/// <summary>
/// A multi-day event that spans more than one day but has specific times
/// </summary>
MultiDay = 2,
/// <summary>
/// A multi-day all-day event (e.g., vacation, conference spanning multiple days)
/// </summary>
MultiDayAllDay = 3,
/// <summary>
/// A recurring event with a defined pattern (daily, weekly, monthly, yearly)
/// </summary>
Recurring = 4,
/// <summary>
/// A recurring all-day event (e.g., annual holiday, weekly all-day event)
/// </summary>
RecurringAllDay = 5,
/// <summary>
/// A single instance of a recurring event that has been modified
/// </summary>
RecurringException = 6,
/// <summary>
/// An event that extends beyond midnight but is not multi-day (e.g., 11 PM to 2 AM)
/// </summary>
CrossMidnight = 7,
}

View File

@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Helpers;
/// <summary>
/// Helper class for CalendarItemType operations
/// </summary>
public static class CalendarItemTypeHelper
{
/// <summary>
/// Determines the calendar item type based on event properties
/// </summary>
/// <param name="startDateTime">Event start date/time</param>
/// <param name="endDateTime">Event end date/time</param>
/// <param name="isAllDay">Whether the event is marked as all-day</param>
/// <param name="isRecurring">Whether the event has recurrence rules</param>
/// <param name="isCancelled">Whether the event is cancelled</param>
/// <param name="status">Event status</param>
/// <returns>The appropriate CalendarItemType</returns>
public static CalendarItemType DetermineItemType(
DateTime startDateTime,
DateTime endDateTime,
bool isAllDay,
bool isRecurring = false,
bool isCancelled = false,
string? status = null)
{
// Handle recurring events
if (isRecurring)
{
return isAllDay ? CalendarItemType.RecurringAllDay : CalendarItemType.Recurring;
}
// Handle all-day events
if (isAllDay)
{
var daySpan = (endDateTime.Date - startDateTime.Date).Days;
return daySpan > 1 ? CalendarItemType.MultiDayAllDay : CalendarItemType.AllDay;
}
// Handle timed events
var duration = endDateTime - startDateTime;
// Multi-day timed events
if (duration.TotalDays >= 1)
{
return CalendarItemType.MultiDay;
}
// Cross midnight events (same calendar day but extends past midnight)
if (startDateTime.Date != endDateTime.Date && duration.TotalHours <= 24)
{
return CalendarItemType.CrossMidnight;
}
// Standard timed events
return CalendarItemType.Timed;
}
/// <summary>
/// Gets a human-readable description of the calendar item type
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>Description string</returns>
public static string GetDescription(CalendarItemType itemType)
{
return itemType switch
{
CalendarItemType.Timed => "Timed Event",
CalendarItemType.AllDay => "All-Day Event",
CalendarItemType.MultiDay => "Multi-Day Event",
CalendarItemType.MultiDayAllDay => "Multi-Day All-Day Event",
CalendarItemType.Recurring => "Recurring Event",
CalendarItemType.RecurringAllDay => "Recurring All-Day Event",
CalendarItemType.RecurringException => "Modified Recurring Event",
CalendarItemType.CrossMidnight => "Cross-Midnight Event",
_ => "Unknown Event Type"
};
}
/// <summary>
/// Checks if the event type represents an all-day event
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>True if it's an all-day event type</returns>
public static bool IsAllDayType(CalendarItemType itemType)
{
return itemType == CalendarItemType.AllDay ||
itemType == CalendarItemType.MultiDayAllDay ||
itemType == CalendarItemType.RecurringAllDay;
}
/// <summary>
/// Checks if the event type represents a recurring event
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>True if it's a recurring event type</returns>
public static bool IsRecurringType(CalendarItemType itemType)
{
return itemType == CalendarItemType.Recurring ||
itemType == CalendarItemType.RecurringAllDay ||
itemType == CalendarItemType.RecurringException;
}
/// <summary>
/// Checks if the event type represents a multi-day event
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>True if it's a multi-day event type</returns>
public static bool IsMultiDayType(CalendarItemType itemType)
{
return itemType == CalendarItemType.MultiDay ||
itemType == CalendarItemType.MultiDayAllDay;
}
/// <summary>
/// Gets the priority level for sorting events (lower number = higher priority)
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>Priority number for sorting</returns>
public static int GetSortPriority(CalendarItemType itemType)
{
return itemType switch
{
CalendarItemType.AllDay => 2,
CalendarItemType.MultiDayAllDay => 3,
CalendarItemType.Timed => 4,
CalendarItemType.CrossMidnight => 5,
CalendarItemType.MultiDay => 6,
CalendarItemType.Recurring => 7,
CalendarItemType.RecurringAllDay => 8,
CalendarItemType.RecurringException => 9,
_ => 99
};
}
}

View File

@@ -1,5 +1,6 @@
using System;
using Itenso.TimePeriod;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
@@ -8,15 +9,8 @@ public interface ICalendarItem
string Title { get; }
Guid Id { get; }
IAccountCalendar AssignedCalendar { get; }
DateTime StartDate { get; set; }
DateTime EndDate { get; }
double DurationInSeconds { get; set; }
DateTime StartDateTime { get; set; }
DateTime EndDateTime { get; }
ITimePeriod Period { get; }
bool IsAllDayEvent { get; }
bool IsMultiDayEvent { get; }
bool IsRecurringChild { get; }
bool IsRecurringParent { get; }
bool IsRecurringEvent { get; }
CalendarItemType ItemType { get; }
}

View File

@@ -1,9 +1,13 @@
namespace Wino.Core.Domain.Interfaces;
using System;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
/// </summary>
public interface ICalendarItemViewModel
public interface ICalendarItemViewModel : ICalendarItem
{
bool IsSelected { get; set; }
DateTime LocalStartDateTime { get; }
DateTime LocalEndDateTime { get; }
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
public interface ICalendarServiceEx
{
Task<int> ClearAllCalendarEventAttendeesAsync();
Task<int> ClearAllCalendarsAsync();
Task<int> ClearAllDataAsync();
Task<int> ClearAllEventsAsync();
Task<int> DeleteCalendarAsync(string remoteCalendarId);
Task<int> DeleteCalendarEventAttendeesForEventAsync(Guid eventId);
Task<int> DeleteEventAsync(string remoteEventId);
Task<List<CalendarEventAttendee>> GetAllCalendarEventAttendeesAsync();
Task<List<AccountCalendar>> GetAllCalendarsAsync();
Task<List<CalendarItem>> GetAllDayEventsAsync();
Task<List<CalendarItem>> GetAllEventsAsync();
Task<List<CalendarItem>> GetAllEventsIncludingDeletedAsync();
Task<List<CalendarItem>> GetAllRecurringEventsByTypeAsync();
Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId);
Task<Dictionary<AttendeeResponseStatus, int>> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId);
Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventAsync(Guid eventId);
Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId);
Task<string> GetCalendarSyncTokenAsync(string calendarId);
Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId);
Task<List<CalendarItem>> GetEventsByItemTypeAsync(CalendarItemType itemType);
Task<List<CalendarItem>> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes);
Task<List<CalendarItem>> GetEventsByremoteCalendarIdAsync(string remoteCalendarId);
Task<List<CalendarItem>> GetEventsForCalendarAsync(Guid calendarId);
Task<List<CalendarItem>> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate);
Task<List<CalendarItem>> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime);
Task<Dictionary<CalendarItemType, int>> GetEventStatsByItemTypeAsync();
Task<List<CalendarItem>> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate);
Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar);
Task<DateTime?> GetLastSyncTimeAsync(string calendarId);
Task<List<CalendarItem>> GetMultiDayEventsAsync();
Task<List<CalendarItem>> GetRecurringEventsAsync();
Task<int> HardDeleteEventAsync(string remoteEventId);
Task<int> InsertCalendarAsync(AccountCalendar calendar);
Task<int> InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee);
Task<int> InsertEventAsync(CalendarItem calendarItem);
Task<int> MarkEventAsDeletedAsync(string remoteEventId, string remoteCalendarId);
Task<int> SyncAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> attendees);
Task<int> SyncCalendarEventAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> calendareventattendees);
Task<int> UpdateAllEventItemTypesAsync();
Task<int> UpdateCalendarAsync(AccountCalendar calendar);
Task<int> UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee);
Task<int> UpdateCalendarSyncTokenAsync(string calendarId, string syncToken);
Task<int> UpdateEventAsync(CalendarItem calendarItem);
Task<int> UpsertCalendarAsync(AccountCalendar calendar);
Task<int> UpsertEventAsync(CalendarItem calendarItem);
}

View File

@@ -5,7 +5,6 @@ using System.Web;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Gmail.v1.Data;
using MimeKit;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -179,6 +178,11 @@ public static class GoogleIntegratorExtensions
Id = Guid.NewGuid(),
TimeZone = calendarListEntry.TimeZone,
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
Description = calendarListEntry.Description,
AccessRole = calendarListEntry.AccessRole,
CreatedDate = DateTime.UtcNow,
LastSyncTime = DateTime.UtcNow,
Location = calendarListEntry.Location,
};
// Bg color must present. Generate one if doesnt exists.
@@ -190,42 +194,121 @@ public static class GoogleIntegratorExtensions
return calendar;
}
public static DateTimeOffset? GetEventDateTimeOffset(EventDateTime calendarEvent)
public static CalendarItem MapGoogleEventToCalendarEvent(this Event googleEvent, AccountCalendar calendar)
{
if (calendarEvent != null)
var calendarEvent = new CalendarItem
{
if (calendarEvent.DateTimeDateTimeOffset != null)
RemoteEventId = googleEvent.Id,
CalendarId = calendar.Id, // Use internal Guid
Title = googleEvent.Summary ?? string.Empty,
Description = googleEvent.Description,
Location = googleEvent.Location,
Status = googleEvent.Status,
RecurringEventId = googleEvent.RecurringEventId
};
// Handle start and end times
if (googleEvent.Start != null)
{
return calendarEvent.DateTimeDateTimeOffset.Value;
if (googleEvent.Start.Date != null)
{
calendarEvent.IsAllDay = true;
calendarEvent.StartDateTime = DateTime.Parse(googleEvent.Start.Date);
}
else if (calendarEvent.Date != null)
else if (googleEvent.Start.DateTimeDateTimeOffset.HasValue)
{
if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime))
{
// Date-only events are treated as UTC midnight
return new DateTimeOffset(eventDateTime, TimeSpan.Zero);
}
else
{
throw new Exception("Invalid date format in Google Calendar event date.");
}
calendarEvent.IsAllDay = false;
calendarEvent.StartDateTime = googleEvent.Start.DateTimeDateTimeOffset.Value.DateTime;
calendarEvent.TimeZone = googleEvent.Start.TimeZone;
}
}
return null;
if (googleEvent.End != null)
{
if (googleEvent.End.Date != null)
{
calendarEvent.EndDateTime = DateTime.Parse(googleEvent.End.Date);
}
else if (googleEvent.End.DateTimeDateTimeOffset.HasValue)
{
calendarEvent.EndDateTime = googleEvent.End.DateTimeDateTimeOffset.Value.DateTime;
}
}
// Handle recurrence rules
if (googleEvent.Recurrence != null && googleEvent.Recurrence.Count > 0)
{
calendarEvent.RecurrenceRules = string.Join(";", googleEvent.Recurrence);
}
// Handle organizer
if (googleEvent.Organizer != null)
{
calendarEvent.OrganizerDisplayName = googleEvent.Organizer.DisplayName;
calendarEvent.OrganizerEmail = googleEvent.Organizer.Email;
}
// Handle timestamps
if (googleEvent.CreatedDateTimeOffset.HasValue)
{
calendarEvent.CreatedDate = googleEvent.CreatedDateTimeOffset.Value.DateTime;
}
if (googleEvent.UpdatedDateTimeOffset.HasValue)
{
calendarEvent.LastModified = googleEvent.UpdatedDateTimeOffset.Value.DateTime;
}
// Handle original start time for recurring event instances
if (googleEvent.OriginalStartTime != null)
{
if (googleEvent.OriginalStartTime.Date != null)
{
calendarEvent.OriginalStartTime = googleEvent.OriginalStartTime.Date;
}
else if (googleEvent.OriginalStartTime.DateTimeDateTimeOffset.HasValue)
{
calendarEvent.OriginalStartTime = googleEvent.OriginalStartTime.DateTimeDateTimeOffset.Value.ToString("O");
}
}
// Automatically determine the calendar item type based on event properties
calendarEvent.DetermineItemType();
return calendarEvent;
}
/// <summary>
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
/// Converts a Google Calendar API response status string to AttendeeResponseStatus enum
/// </summary>
/// <returns>___ separated lines.</returns>
public static string GetRecurrenceString(this Event calendarEvent)
/// <param name="googleStatus">The status string from Google Calendar API</param>
/// <returns>Corresponding AttendeeResponseStatus enum value</returns>
public static AttendeeResponseStatus FromGoogleStatus(string? googleStatus)
{
if (calendarEvent == null || calendarEvent.Recurrence == null || !calendarEvent.Recurrence.Any())
return googleStatus?.ToLowerInvariant() switch
{
return null;
"accepted" => AttendeeResponseStatus.Accepted,
"declined" => AttendeeResponseStatus.Declined,
"tentative" => AttendeeResponseStatus.Tentative,
"needsaction" => AttendeeResponseStatus.NeedsAction,
_ => AttendeeResponseStatus.NeedsAction
};
}
return string.Join(Constants.CalendarEventRecurrenceRuleSeperator, calendarEvent.Recurrence);
/// <summary>
/// Converts an AttendeeResponseStatus enum to Google Calendar API response status string
/// </summary>
/// <param name="status">The AttendeeResponseStatus enum value</param>
/// <returns>Corresponding Google Calendar API status string</returns>
public static string ToGoogleStatus(AttendeeResponseStatus status)
{
return status switch
{
AttendeeResponseStatus.Accepted => "accepted",
AttendeeResponseStatus.Declined => "declined",
AttendeeResponseStatus.Tentative => "tentative",
AttendeeResponseStatus.NeedsAction => "needsAction",
_ => "needsAction"
};
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Graph.Models;
using MimeKit;
using Wino.Core.Domain.Entities.Calendar;
@@ -127,125 +126,26 @@ public static class OutlookIntegratorExtensions
return calendar;
}
private static string GetRfc5545DayOfWeek(DayOfWeekObject dayOfWeek)
/// <summary>
/// Converts Outlook response status to our enum
/// </summary>
/// <param name="outlookResponse">Outlook response type</param>
/// <returns>AttendeeResponseStatus enum value</returns>
public static AttendeeResponseStatus ConvertOutlookResponseStatus(this Microsoft.Graph.Models.ResponseType? outlookResponse)
{
return dayOfWeek switch
return outlookResponse switch
{
DayOfWeekObject.Monday => "MO",
DayOfWeekObject.Tuesday => "TU",
DayOfWeekObject.Wednesday => "WE",
DayOfWeekObject.Thursday => "TH",
DayOfWeekObject.Friday => "FR",
DayOfWeekObject.Saturday => "SA",
DayOfWeekObject.Sunday => "SU",
_ => throw new ArgumentOutOfRangeException(nameof(dayOfWeek), dayOfWeek, null)
Microsoft.Graph.Models.ResponseType.Accepted => AttendeeResponseStatus.Accepted,
Microsoft.Graph.Models.ResponseType.Declined => AttendeeResponseStatus.Declined,
Microsoft.Graph.Models.ResponseType.TentativelyAccepted => AttendeeResponseStatus.Tentative,
Microsoft.Graph.Models.ResponseType.None => AttendeeResponseStatus.NeedsAction,
Microsoft.Graph.Models.ResponseType.NotResponded => AttendeeResponseStatus.NeedsAction,
_ => AttendeeResponseStatus.NeedsAction
};
}
public static string ToRfc5545RecurrenceString(this PatternedRecurrence recurrence)
{
if (recurrence == null || recurrence.Pattern == null)
throw new ArgumentNullException(nameof(recurrence), "PatternedRecurrence or its Pattern cannot be null.");
var ruleBuilder = new StringBuilder("RRULE:");
var pattern = recurrence.Pattern;
// Frequency
switch (pattern.Type)
{
case RecurrencePatternType.Daily:
ruleBuilder.Append("FREQ=DAILY;");
break;
case RecurrencePatternType.Weekly:
ruleBuilder.Append("FREQ=WEEKLY;");
break;
case RecurrencePatternType.AbsoluteMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.AbsoluteYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
case RecurrencePatternType.RelativeMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.RelativeYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
default:
throw new NotSupportedException($"Unsupported recurrence pattern type: {pattern.Type}");
}
// Interval
if (pattern.Interval > 0)
ruleBuilder.Append($"INTERVAL={pattern.Interval};");
// Days of Week
if (pattern.DaysOfWeek?.Any() == true)
{
var days = string.Join(",", pattern.DaysOfWeek.Select(day => day.ToString().ToUpperInvariant().Substring(0, 2)));
ruleBuilder.Append($"BYDAY={days};");
}
// Day of Month (BYMONTHDAY)
if (pattern.Type == RecurrencePatternType.AbsoluteMonthly || pattern.Type == RecurrencePatternType.AbsoluteYearly)
{
if (pattern.DayOfMonth <= 0)
throw new ArgumentException("DayOfMonth must be greater than 0 for absoluteMonthly or absoluteYearly patterns.");
ruleBuilder.Append($"BYMONTHDAY={pattern.DayOfMonth};");
}
// Month (BYMONTH)
if (pattern.Type == RecurrencePatternType.AbsoluteYearly || pattern.Type == RecurrencePatternType.RelativeYearly)
{
if (pattern.Month <= 0)
throw new ArgumentException("Month must be greater than 0 for absoluteYearly or relativeYearly patterns.");
ruleBuilder.Append($"BYMONTH={pattern.Month};");
}
// Count or Until
if (recurrence.Range != null)
{
if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null)
{
ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};");
}
else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue)
{
ruleBuilder.Append($"COUNT={recurrence.Range.NumberOfOccurrences.Value};");
}
}
// Remove trailing semicolon
return ruleBuilder.ToString().TrimEnd(';');
}
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
{
throw new ArgumentException("DateTimeTimeZone is null or empty.");
}
try
{
// Parse the DateTime string
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
{
// Get TimeZoneInfo to get the offset
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
return new DateTimeOffset(parsedDateTime, offset);
}
else
throw new ArgumentException("DateTime string is not in a valid format.");
}
catch (Exception)
{
throw;
}
}
private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType)
{
@@ -261,24 +161,6 @@ public static class OutlookIntegratorExtensions
};
}
public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId)
{
bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer;
var eventAttendee = new CalendarEventAttendee()
{
CalendarItemId = calendarItemId,
Id = Guid.NewGuid(),
Email = attendee.EmailAddress?.Address,
Name = attendee.EmailAddress?.Name,
AttendenceStatus = GetAttendeeStatus(attendee.Status.Response),
IsOrganizer = isOrganizer,
IsOptionalAttendee = attendee.Type == AttendeeType.Optional,
};
return eventAttendee;
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)

View File

@@ -21,7 +21,7 @@ namespace Wino.Core.Integration.Processors;
/// <see cref="IGmailChangeProcessor"/>, <see cref="IOutlookChangeProcessor"/> and <see cref="IImapChangeProcessor"/>
/// None of the synchronizers can directly change anything in the database.
/// </summary>
public interface IDefaultChangeProcessor
public interface IDefaultChangeProcessor : ICalendarServiceEx
{
Task UpdateAccountAsync(MailAccount account);
// Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
@@ -113,13 +113,14 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor, ICalendarServiceEx
{
protected IMailService MailService = mailService;
protected ICalendarService CalendarService = calendarService;
protected IFolderService FolderService = folderService;
protected IAccountService AccountService = accountService;
private readonly ICalendarServiceEx _calendarServiceEx = calendarServiceEx;
private readonly IMimeFileService _mimeFileService = mimeFileService;
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
@@ -208,4 +209,234 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);
// TODO: Normalize this shit. Not everything needs to be exposed for processor.
#region ICalendarServiceEx
public Task<int> ClearAllCalendarEventAttendeesAsync()
{
return _calendarServiceEx.ClearAllCalendarEventAttendeesAsync();
}
public Task<int> ClearAllCalendarsAsync()
{
return _calendarServiceEx.ClearAllCalendarsAsync();
}
public Task<int> ClearAllDataAsync()
{
return _calendarServiceEx.ClearAllDataAsync();
}
public Task<int> ClearAllEventsAsync()
{
return _calendarServiceEx.ClearAllEventsAsync();
}
public Task<int> DeleteCalendarAsync(string remoteCalendarId)
{
return _calendarServiceEx.DeleteCalendarAsync(remoteCalendarId);
}
public Task<int> DeleteCalendarEventAttendeesForEventAsync(Guid eventId)
{
return _calendarServiceEx.DeleteCalendarEventAttendeesForEventAsync(eventId);
}
public Task<int> DeleteEventAsync(string remoteEventId)
{
return _calendarServiceEx.DeleteEventAsync(remoteEventId);
}
public Task<List<CalendarEventAttendee>> GetAllCalendarEventAttendeesAsync()
{
return _calendarServiceEx.GetAllCalendarEventAttendeesAsync();
}
public Task<List<AccountCalendar>> GetAllCalendarsAsync()
{
return _calendarServiceEx.GetAllCalendarsAsync();
}
public Task<List<CalendarItem>> GetAllDayEventsAsync()
{
return _calendarServiceEx.GetAllDayEventsAsync();
}
public Task<List<CalendarItem>> GetAllEventsAsync()
{
return _calendarServiceEx.GetAllEventsAsync();
}
public Task<List<CalendarItem>> GetAllEventsIncludingDeletedAsync()
{
return _calendarServiceEx.GetAllEventsIncludingDeletedAsync();
}
public Task<List<CalendarItem>> GetAllRecurringEventsByTypeAsync()
{
return _calendarServiceEx.GetAllRecurringEventsByTypeAsync();
}
public Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId)
{
return _calendarServiceEx.GetCalendarByRemoteIdAsync(remoteCalendarId);
}
public Task<Dictionary<AttendeeResponseStatus, int>> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId)
{
return _calendarServiceEx.GetCalendarEventAttendeeResponseCountsAsync(eventId);
}
public Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventAsync(Guid eventId)
{
return _calendarServiceEx.GetCalendarEventAttendeesForEventAsync(eventId);
}
public Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId)
{
return _calendarServiceEx.GetCalendarEventAttendeesForEventByRemoteIdAsync(remoteEventId);
}
public Task<string> GetCalendarSyncTokenAsync(string calendarId)
{
return _calendarServiceEx.GetCalendarSyncTokenAsync(calendarId);
}
public Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId)
{
return _calendarServiceEx.GetEventByRemoteIdAsync(remoteEventId);
}
public Task<List<CalendarItem>> GetEventsByItemTypeAsync(CalendarItemType itemType)
{
return _calendarServiceEx.GetEventsByItemTypeAsync(itemType);
}
public Task<List<CalendarItem>> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes)
{
return _calendarServiceEx.GetEventsByItemTypesAsync(itemTypes);
}
public Task<List<CalendarItem>> GetEventsByremoteCalendarIdAsync(string remoteCalendarId)
{
return _calendarServiceEx.GetEventsByremoteCalendarIdAsync(remoteCalendarId);
}
public Task<List<CalendarItem>> GetEventsForCalendarAsync(Guid calendarId)
{
return _calendarServiceEx.GetEventsForCalendarAsync(calendarId);
}
public Task<List<CalendarItem>> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
{
return _calendarServiceEx.GetEventsInDateRangeAsync(startDate, endDate);
}
public Task<List<CalendarItem>> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime)
{
return _calendarServiceEx.GetEventsSinceLastSyncAsync(lastSyncTime);
}
public Task<Dictionary<CalendarItemType, int>> GetEventStatsByItemTypeAsync()
{
return _calendarServiceEx.GetEventStatsByItemTypeAsync();
}
public Task<List<CalendarItem>> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
{
return _calendarServiceEx.GetExpandedEventsInDateRangeAsync(startDate, endDate);
}
public Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar)
{
return _calendarServiceEx.GetExpandedEventsInDateRangeWithExceptionsAsync(startDate, endDate, calendar);
}
public Task<DateTime?> GetLastSyncTimeAsync(string calendarId)
{
return _calendarServiceEx.GetLastSyncTimeAsync(calendarId);
}
public Task<List<CalendarItem>> GetMultiDayEventsAsync()
{
return _calendarServiceEx.GetMultiDayEventsAsync();
}
public Task<List<CalendarItem>> GetRecurringEventsAsync()
{
return _calendarServiceEx.GetRecurringEventsAsync();
}
public Task<int> HardDeleteEventAsync(string remoteEventId)
{
return _calendarServiceEx.HardDeleteEventAsync(remoteEventId);
}
public Task<int> InsertCalendarAsync(AccountCalendar calendar)
{
return _calendarServiceEx.InsertCalendarAsync(calendar);
}
public Task<int> InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
{
return _calendarServiceEx.InsertCalendarEventAttendeeAsync(calendareventattendee);
}
public Task<int> InsertEventAsync(CalendarItem calendarItem)
{
return _calendarServiceEx.InsertEventAsync(calendarItem);
}
public Task<int> MarkEventAsDeletedAsync(string remoteEventId, string remoteCalendarId)
{
return _calendarServiceEx.MarkEventAsDeletedAsync(remoteEventId, remoteCalendarId);
}
public Task<int> SyncCalendarEventAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> calendareventattendees)
{
return _calendarServiceEx.SyncCalendarEventAttendeesForEventAsync(eventId, calendareventattendees);
}
public Task<int> UpdateAllEventItemTypesAsync()
{
return _calendarServiceEx.UpdateAllEventItemTypesAsync();
}
public Task<int> UpdateCalendarAsync(AccountCalendar calendar)
{
return _calendarServiceEx.UpdateCalendarAsync(calendar);
}
public Task<int> UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
{
return _calendarServiceEx.UpdateCalendarEventAttendeeAsync(calendareventattendee);
}
public Task<int> UpdateCalendarSyncTokenAsync(string calendarId, string syncToken)
{
return _calendarServiceEx.UpdateCalendarSyncTokenAsync(calendarId, syncToken);
}
public Task<int> UpdateEventAsync(CalendarItem calendarItem)
{
return _calendarServiceEx.UpdateEventAsync(calendarItem);
}
public Task<int> UpsertCalendarAsync(AccountCalendar calendar)
{
return _calendarServiceEx.UpsertCalendarAsync(calendar);
}
public Task<int> UpsertEventAsync(CalendarItem calendarItem)
{
return _calendarServiceEx.UpsertEventAsync(calendarItem);
}
public Task<int> SyncAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> attendees)
{
return _calendarServiceEx.SyncAttendeesForEventAsync(eventId, attendees);
}
#endregion
}

View File

@@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Google.Apis.Calendar.v3.Data;
using Serilog;
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.Domain.Models.MailItem;
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;
@@ -23,7 +18,8 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
{
}
@@ -35,226 +31,9 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
=> MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
var status = calendarEvent.Status;
var recurringEventId = calendarEvent.RecurringEventId;
// 1. Canceled exceptions of recurred events are only guaranteed to have recurringEventId, Id and start time.
// 2. Updated exceptions of recurred events have different Id, but recurringEventId is the same as parent.
// Check if we have this event before.
var existingCalendarItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
if (existingCalendarItem == null)
{
CalendarItem parentRecurringEvent = null;
// Manage the recurring event id.
if (!string.IsNullOrEmpty(recurringEventId))
{
parentRecurringEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, recurringEventId).ConfigureAwait(false);
if (parentRecurringEvent == null)
{
Log.Information($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// We don't have this event yet. Create a new one.
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
double totalDurationInSeconds = 0;
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
{
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
}
CalendarItem calendarItem = null;
if (parentRecurringEvent != null)
{
// Exceptions of parent events might not have all the fields populated.
// We must use the parent event's data for fields that don't exists.
// Update duration if it's not populated.
if (totalDurationInSeconds == 0)
{
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
}
var organizerMail = GetOrganizerEmail(calendarEvent, organizerAccount);
var organizerName = GetOrganizerName(calendarEvent, organizerAccount);
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset,
DurationInSeconds = totalDurationInSeconds,
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
// Leave it empty if it's not populated.
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility),
HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = string.IsNullOrEmpty(organizerName) ? parentRecurringEvent.OrganizerDisplayName : organizerName,
OrganizerEmail = string.IsNullOrEmpty(organizerMail) ? parentRecurringEvent.OrganizerEmail : organizerMail
};
}
else
{
// This is a parent event creation.
// Start-End dates are guaranteed to be populated.
if (eventStartDateTimeOffset == null || eventEndDateTimeOffset == null)
{
Log.Error("Failed to create parent event because either start or end date is not specified.");
return;
}
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location,
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = GetVisibility(calendarEvent.Visibility),
HtmlLink = calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = GetOrganizerName(calendarEvent, organizerAccount),
OrganizerEmail = GetOrganizerEmail(calendarEvent, organizerAccount)
};
}
// Hide canceled events.
calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled;
// Manage the recurring event id.
if (parentRecurringEvent != null)
{
calendarItem.RecurringCalendarItemId = parentRecurringEvent.Id;
}
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
// 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);
}
else
{
// We have this event already. Update it.
if (calendarEvent.Status == "cancelled")
{
// Parent event is canceled. We must delete everything.
if (string.IsNullOrEmpty(recurringEventId))
{
Log.Information("Parent event is canceled. Deleting all instances of {Id}", existingCalendarItem.Id);
await CalendarService.DeleteCalendarItemAsync(existingCalendarItem.Id).ConfigureAwait(false);
return;
}
else
{
// Child event is canceled.
// Child should live as long as parent lives, but must not be displayed to the user.
existingCalendarItem.IsHidden = true;
}
}
else
{
// Make sure to unhide the event.
// It might be marked as hidden before.
existingCalendarItem.IsHidden = false;
// Update the event properties.
}
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(existingCalendarItem);
}
private string GetOrganizerName(Event calendarEvent, MailAccount account)
{

View File

@@ -13,7 +13,8 @@ public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor
IMailService mailService,
IAccountService accountService,
ICalendarService calendarService,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
{
}

View File

@@ -1,13 +1,9 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Graph.Models;
using Serilog;
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;
namespace Wino.Core.Integration.Processors;
@@ -17,7 +13,8 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
ICalendarService calendarService,
IMailService mailService,
IAccountService accountService,
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
, IOutlookChangeProcessor
{
@@ -40,106 +37,6 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// We parse the occurrences based on the parent event.
// There is literally no point to store them because
// type=Exception events are the exceptional childs of recurrency parent event.
if (calendarEvent.Type == EventType.Occurrence) return;
var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
Guid savingItemId = Guid.Empty;
if (savingItem != null)
savingItemId = savingItem.Id;
else
{
savingItemId = Guid.NewGuid();
savingItem = new CalendarItem() { Id = savingItemId };
}
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
savingItem.RemoteEventId = calendarEvent.Id;
savingItem.StartDate = eventStartDateTimeOffset.DateTime;
savingItem.StartDateOffset = eventStartDateTimeOffset.Offset;
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
savingItem.DurationInSeconds = durationInSeconds;
savingItem.Title = calendarEvent.Subject;
savingItem.Description = calendarEvent.Body?.Content;
savingItem.Location = calendarEvent.Location?.DisplayName;
if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId))
{
// This is a recurring event exception.
// We need to find the parent event and set it as recurring event id.
var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId);
if (parentEvent != null)
{
savingItem.RecurringCalendarItemId = parentEvent.Id;
}
else
{
Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// Convert the recurrence pattern to string for parent recurring events.
if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null)
{
savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence);
}
savingItem.HtmlLink = calendarEvent.WebLink;
savingItem.CalendarId = assignedCalendar.Id;
savingItem.OrganizerEmail = calendarEvent.Organizer?.EmailAddress?.Address;
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
savingItem.IsHidden = false;
if (calendarEvent.ResponseStatus?.Response != null)
{
switch (calendarEvent.ResponseStatus.Response.Value)
{
case ResponseType.None:
case ResponseType.NotResponded:
savingItem.Status = CalendarItemStatus.NotResponded;
break;
case ResponseType.TentativelyAccepted:
savingItem.Status = CalendarItemStatus.Tentative;
break;
case ResponseType.Accepted:
case ResponseType.Organizer:
savingItem.Status = CalendarItemStatus.Confirmed;
break;
case ResponseType.Declined:
savingItem.Status = CalendarItemStatus.Cancelled;
savingItem.IsHidden = true;
break;
default:
break;
}
}
else
{
savingItem.Status = CalendarItemStatus.Confirmed;
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(savingItem);
// Manage attendees.
if (calendarEvent.Attendees != null)
{
// Clear all attendees for this event.
var attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList();
await CalendarService.ManageEventAttendeesAsync(savingItemId, attendees).ConfigureAwait(false);
}
// TODO
}
}

View File

@@ -361,75 +361,207 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
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);
// We can do just delta sync. It will fallback to full sync if there are no sync tokens or if the token is expired.
await DeltaSynchronizeCalendarAsync(calendar).ConfigureAwait(false);
}
request.SingleEvents = false;
request.ShowDeleted = true;
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
// TODO: Return proper result from delta or full sync.
return new CalendarSynchronizationResult()
{
// If a sync token is available, perform an incremental sync
request.SyncToken = calendar.SynchronizationDeltaToken;
CompletedState = SynchronizationCompletedState.Success,
DownloadedEvents = new List<ICalendarItem>(),
};
}
private async Task FullSynchronizeCalendarAsync(AccountCalendar calendar)
{
var calendarId = calendar.RemoteCalendarId;
try
{
// Get events from the last 30 days to 1 year in the future
var timeMin = DateTime.Now.AddYears(-3);
var timeMax = DateTime.Now.AddYears(2);
var request = _calendarService.Events.List(calendarId);
request.TimeMinDateTimeOffset = timeMin;
request.TimeMaxDateTimeOffset = timeMax;
request.SingleEvents = false; // Include recurring events
request.ShowDeleted = true; // Include deleted events for synchronization
request.MaxResults = 2500; // Maximum allowed by Google Calendar API
var events = await request.ExecuteAsync();
if (events.Items != null && events.Items.Count > 0)
{
Console.WriteLine($"Processing {events.Items.Count} events from calendar: {calendarId}");
foreach (var googleEvent in events.Items)
{
await ProcessGoogleEventAsync(googleEvent, calendar);
}
}
else
{
// If no sync token, perform an initial sync
// Fetch events from the past year
request.TimeMinDateTimeOffset = DateTimeOffset.UtcNow.AddYears(-1);
Console.WriteLine($"No events found in calendar: {calendarId}");
}
string nextPageToken;
string syncToken;
var allEvents = new List<Event>();
do
// Store the sync token for future delta syncs
if (!string.IsNullOrEmpty(events.NextSyncToken))
{
// Execute the request
var events = await request.ExecuteAsync();
await _gmailChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, events.NextSyncToken).ConfigureAwait(false);
// Process the fetched events
if (events.Items != null)
Console.WriteLine($"Stored sync token for calendar {calendarId} to enable delta sync");
}
}
catch (Exception ex)
{
allEvents.AddRange(events.Items);
throw new InvalidOperationException($"Failed to synchronize calendar {calendarId}: {ex.Message}", ex);
}
}
// 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;
// allEvents contains new or updated events.
// Process them and create/update local calendar items.
foreach (var @event in allEvents)
private async Task<bool> DeltaSynchronizeCalendarAsync(AccountCalendar calendar)
{
// TODO: Exception handling for event processing.
// TODO: Also update attendees and other properties.
var calendarId = calendar.RemoteCalendarId;
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
try
{
Console.WriteLine($"Starting delta sync for calendar: {calendarId}");
// Get the stored sync token for this calendar
var syncToken = calendar.SynchronizationDeltaToken;
if (string.IsNullOrEmpty(syncToken))
{
Console.WriteLine($"No sync token found for calendar {calendarId}. Performing full sync...");
await FullSynchronizeCalendarAsync(calendar);
return true;
}
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
// Create the events list request with sync token
var request = _calendarService.Events.List(calendarId);
request.SyncToken = syncToken;
request.ShowDeleted = true; // Important: include deleted events for delta sync
request.SingleEvents = false; // Include recurring events
Console.WriteLine($"Requesting delta changes with sync token: {syncToken.Substring(0, Math.Min(20, syncToken.Length))}...");
Events events;
try
{
events = await request.ExecuteAsync();
}
catch (Google.GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.Gone)
{
// Sync token has expired, need to do full sync
Console.WriteLine($"Sync token expired for calendar {calendarId}. Performing full sync...");
await FullSynchronizeCalendarAsync(calendar);
return true;
}
return default;
if (events.Items != null && events.Items.Count > 0)
{
Console.WriteLine($"Processing {events.Items.Count} delta changes for calendar: {calendarId}");
foreach (var googleEvent in events.Items)
{
await ProcessDeltaCalendarEventAsync(googleEvent, calendar);
}
}
else
{
Console.WriteLine($"No changes found for calendar: {calendarId}");
}
// Store the new sync token
if (!string.IsNullOrEmpty(events.NextSyncToken))
{
await _gmailChangeProcessor.UpdateCalendarSyncTokenAsync(calendarId, events.NextSyncToken).ConfigureAwait(false);
calendar.SynchronizationDeltaToken = events.NextSyncToken;
Console.WriteLine($"Updated sync token for calendar {calendarId}");
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error during delta sync for calendar {calendarId}: {ex.Message}");
return false;
}
}
/// <summary>
/// Processes a single event change from delta synchronization
/// </summary>
/// <param name="googleEvent">The Google Calendar event</param>
/// <param name="calendarId">The ID of the calendar containing the event</param>
private async Task ProcessDeltaCalendarEventAsync(Event googleEvent, AccountCalendar calendar)
{
var calendarId = calendar.RemoteCalendarId;
try
{
if (googleEvent.Status == "cancelled")
{
// Handle deleted/canceled events
await _gmailChangeProcessor.MarkEventAsDeletedAsync(googleEvent.Id, calendarId);
Console.WriteLine($"🗑️ Marked event as deleted: {googleEvent.Summary ?? googleEvent.Id}");
return;
}
// For active events (confirmed, tentative), process normally
var calendarEvent = GoogleIntegratorExtensions.MapGoogleEventToCalendarEvent(googleEvent, calendar);
var result = await _gmailChangeProcessor.UpsertEventAsync(calendarEvent);
// Sync attendees for delta events too
await SyncEventAttendeesAsync(googleEvent, calendarEvent.Id);
if (result > 0)
{
var action = await _gmailChangeProcessor.GetEventByRemoteIdAsync(googleEvent.Id) != null ? "Updated" : "Created";
Console.WriteLine($"✅ {action} event: {calendarEvent.Title} ({calendarEvent.RemoteEventId})");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Failed to process delta event {googleEvent.Id}: {ex.Message}");
}
}
/// <summary>
/// Processes a single Google Calendar event and updates the local database
/// </summary>
/// <param name="googleEvent">The Google Calendar event</param>
/// <param name="calendarId">The ID of the calendar containing the event</param>
private async Task ProcessGoogleEventAsync(Event googleEvent, AccountCalendar calendar)
{
try
{
if (googleEvent.Status == "cancelled")
{
// Handle deleted events
await _gmailChangeProcessor.DeleteEventAsync(googleEvent.Id);
Console.WriteLine($"Marked event as deleted: {googleEvent.Summary ?? googleEvent.Id}");
return;
}
var calendarEvent = googleEvent.MapGoogleEventToCalendarEvent(calendar);
await _gmailChangeProcessor.UpsertEventAsync(calendarEvent);
// Sync attendees separately
await SyncEventAttendeesAsync(googleEvent, calendarEvent.Id);
Console.WriteLine($"Processed event: {calendarEvent.Title} ({calendarEvent.RemoteEventId})");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to process event {googleEvent.Id}: {ex.Message}");
}
}
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
@@ -511,6 +643,40 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}
}
/// <summary>
/// Syncs attendees for an event from Google Calendar data
/// </summary>
/// <param name="googleEvent">The Google Calendar event</param>
/// <param name="eventId">The internal event Guid</param>
private async Task SyncEventAttendeesAsync(Event googleEvent, Guid eventId)
{
var attendees = new List<CalendarEventAttendee>();
if (googleEvent.Attendees != null && googleEvent.Attendees.Count > 0)
{
foreach (var googleAttendee in googleEvent.Attendees)
{
var attendee = new CalendarEventAttendee
{
EventId = eventId,
Email = googleAttendee.Email ?? string.Empty,
DisplayName = googleAttendee.DisplayName,
ResponseStatus = GoogleIntegratorExtensions.FromGoogleStatus(googleAttendee.ResponseStatus),
IsOptional = googleAttendee.Optional ?? false,
IsOrganizer = googleAttendee.Organizer ?? false,
IsSelf = googleAttendee.Self ?? false,
Comment = googleAttendee.Comment,
AdditionalGuests = googleAttendee.AdditionalGuests
};
attendees.Add(attendee);
}
}
// Sync attendees (replaces existing)
await _gmailChangeProcessor.SyncAttendeesForEventAsync(eventId, attendees).ConfigureAwait(false);
}
private async Task InitializeArchiveFolderAsync()
{
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using SqlKata;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Messaging.Client.Calendar;
@@ -58,27 +51,7 @@ public class CalendarService : BaseDatabaseService, ICalendarService
public async Task DeleteCalendarItemAsync(Guid calendarItemId)
{
var calendarItem = await Connection.GetAsync<CalendarItem>(calendarItemId);
if (calendarItem == null) return;
List<CalendarItem> eventsToRemove = new() { calendarItem };
// In case of parent event, delete all child events as well.
if (!string.IsNullOrEmpty(calendarItem.Recurrence))
{
var recurringEvents = await Connection.Table<CalendarItem>().Where(a => a.RecurringCalendarItemId == calendarItemId).ToListAsync().ConfigureAwait(false);
eventsToRemove.AddRange(recurringEvents);
}
foreach (var @event in eventsToRemove)
{
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == @event.Id).ConfigureAwait(false);
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == @event.Id).ConfigureAwait(false);
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(@event));
}
}
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
@@ -98,83 +71,8 @@ public class CalendarService : BaseDatabaseService, ICalendarService
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel)
{
// TODO: We might need to implement caching here.
// I don't know how much of the events we'll have in total, but this logic scans all events every time for given calendar.
var accountEvents = await Connection.Table<CalendarItem>()
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden).ToListAsync();
var result = new List<CalendarItem>();
foreach (var ev in accountEvents)
{
ev.AssignedCalendar = calendar;
// Parse recurrence rules
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(ev.StartDate),
End = new CalDateTime(ev.EndDate),
};
if (string.IsNullOrEmpty(ev.Recurrence))
{
// No recurrence, only check if we fall into the given period.
if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
{
result.Add(ev);
}
}
else
{
// This event has recurrences.
// Wino stores exceptional recurrent events as a separate calendar item, without the recurrence rule.
// Because each instance of recurrent event can have different attendees, properties etc.
// Even though the event is recurrent, each updated instance is a separate calendar item.
// Calculate the all recurrences, and remove the exceptional instances like hidden ones.
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
// Calculate occurrences in the range.
var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
// Get all recurrent exceptional calendar events.
var exceptionalRecurrences = await Connection.Table<CalendarItem>()
.Where(a => a.RecurringCalendarItemId == ev.Id)
.ToListAsync()
.ConfigureAwait(false);
foreach (var occurrence in occurrences)
{
var exactInstanceCheck = exceptionalRecurrences.FirstOrDefault(a =>
a.Period.OverlapsWith(dayRangeRenderModel.Period));
if (exactInstanceCheck == null)
{
// There is no exception for the period.
// Change the instance StartDate and Duration.
var recurrence = ev.CreateRecurrence(occurrence.Period.StartTime.Value, occurrence.Period.Duration.TotalSeconds);
result.Add(recurrence);
}
else
{
// There is a single instance of this recurrent event.
// It will be added as single item if it's not hidden.
// We don't need to do anything here.
}
}
}
}
return result;
// TODO
return new List<CalendarItem>();
}
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
@@ -221,7 +119,7 @@ public class CalendarService : BaseDatabaseService, ICalendarService
}
public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId)
=> Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync();
=> Connection.Table<CalendarEventAttendee>().Where(x => x.EventId == calendarEventTrackingId).ToListAsync();
public async Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees)
{
@@ -230,7 +128,7 @@ public class CalendarService : BaseDatabaseService, ICalendarService
// Clear all attendees.
var query = new Query()
.From(nameof(CalendarEventAttendee))
.Where(nameof(CalendarEventAttendee.CalendarItemId), calendarItemId)
.Where(nameof(CalendarEventAttendee.EventId), calendarItemId)
.AsDelete();
connection.Execute(query.GetRawQuery());
@@ -239,67 +137,12 @@ public class CalendarService : BaseDatabaseService, ICalendarService
connection.InsertAll(allAttendees);
});
return await Connection.Table<CalendarEventAttendee>().Where(a => a.CalendarItemId == calendarItemId).ToListAsync();
return await Connection.Table<CalendarEventAttendee>().Where(a => a.EventId == calendarItemId).ToListAsync();
}
public async Task<CalendarItem> GetCalendarItemTargetAsync(CalendarItemTarget targetDetails)
{
var eventId = targetDetails.Item.Id;
// Get the event by Id first.
var item = await GetCalendarItemAsync(eventId).ConfigureAwait(false);
bool isRecurringChild = targetDetails.Item.IsRecurringChild;
bool isRecurringParent = targetDetails.Item.IsRecurringParent;
if (targetDetails.TargetType == CalendarEventTargetType.Single)
{
if (isRecurringChild)
{
if (item == null)
{
// This is an occurrence of a recurring event.
// They don't exist in db.
return targetDetails.Item;
}
else
{
// Single exception occurrence of recurring event.
// Return the item.
return item;
}
}
else if (isRecurringParent)
{
// Parent recurring events are never listed.
Debugger.Break();
return null;
}
else
{
// Single event.
return item;
}
}
else
{
// Series.
if (isRecurringChild)
{
// Return the parent.
return await GetCalendarItemAsync(targetDetails.Item.RecurringCalendarItemId.Value).ConfigureAwait(false);
}
else if (isRecurringParent)
return item;
else
{
// NA. Single events don't have series.
Debugger.Break();
// TODO
return null;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ public static class ServicesContainerSetup
services.AddSingleton<IMimeFileService, MimeFileService>();
services.AddTransient<ICalendarService, CalendarService>();
services.AddTransient<ICalendarServiceEx, CalendarServiceEx>();
services.AddTransient<IMailService, MailService>();
services.AddTransient<IFolderService, FolderService>();
services.AddTransient<IAccountService, AccountService>();
@@ -29,7 +30,5 @@ public static class ServicesContainerSetup
services.AddTransient<IOutlookThreadingStrategy, OutlookThreadingStrategy>();
services.AddTransient<IGmailThreadingStrategy, GmailThreadingStrategy>();
services.AddTransient<IImapThreadingStrategy, ImapThreadingStrategy>();
}
}