Compare commits
3 Commits
main
...
feature/Ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6db85bfa39 | ||
|
|
03c9ac1e11 | ||
|
|
1ee0063b62 |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,19 +789,18 @@ 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
|
||||
{
|
||||
return DayRanges
|
||||
.SelectMany(a => a.CalendarDays)
|
||||
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
|
||||
.Where(c => c != null)
|
||||
.Cast<CalendarItemViewModel>()
|
||||
.Distinct();
|
||||
}
|
||||
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))
|
||||
.Where(c => c != null)
|
||||
.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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="Transparent"
|
||||
IsTabStop="False"
|
||||
PlaceholderText="Search" />
|
||||
|
||||
<StackPanel
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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 starts at midnight and ends at midnight are considered all-day events.
|
||||
/// The type of calendar item (Timed, AllDay, MultiDay, etc.)
|
||||
/// </summary>
|
||||
public bool IsAllDayEvent
|
||||
public CalendarItemType ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Automatically determines and sets the ItemType based on event properties
|
||||
/// </summary>
|
||||
public void DetermineItemType()
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
StartDate.TimeOfDay == TimeSpan.Zero &&
|
||||
EndDate.TimeOfDay == TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
var hasRecurrence = !string.IsNullOrEmpty(RecurrenceRules);
|
||||
var isCancelled = Status?.ToLowerInvariant() == "cancelled" || IsDeleted;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public bool IsRecurringChild
|
||||
{
|
||||
get
|
||||
{
|
||||
return RecurringCalendarItemId != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events that are either an exceptional instance of a recurring event or occurrences.
|
||||
/// </summary>
|
||||
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
32
Wino.Core.Domain/Enums/AttendeeResponseStatus.cs
Normal file
32
Wino.Core.Domain/Enums/AttendeeResponseStatus.cs
Normal 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
|
||||
}
|
||||
49
Wino.Core.Domain/Enums/CalendarItemType.cs
Normal file
49
Wino.Core.Domain/Enums/CalendarItemType.cs
Normal 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,
|
||||
}
|
||||
143
Wino.Core.Domain/Helpers/CalendarItemTypeHelper.cs
Normal file
143
Wino.Core.Domain/Helpers/CalendarItemTypeHelper.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
55
Wino.Core.Domain/Interfaces/ICalendarServiceEx.cs
Normal file
55
Wino.Core.Domain/Interfaces/ICalendarServiceEx.cs
Normal 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);
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
if (googleEvent.Start.Date != null)
|
||||
{
|
||||
return calendarEvent.DateTimeDateTimeOffset.Value;
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -36,224 +32,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
|
||||
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);
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private string GetOrganizerName(Event calendarEvent, MailAccount account)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// TODO: Return proper result from delta or full sync.
|
||||
return new CalendarSynchronizationResult()
|
||||
{
|
||||
CompletedState = SynchronizationCompletedState.Success,
|
||||
DownloadedEvents = new List<ICalendarItem>(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
|
||||
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)
|
||||
{
|
||||
// If a sync token is available, perform an incremental sync
|
||||
request.SyncToken = calendar.SynchronizationDeltaToken;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
// allEvents contains new or updated events.
|
||||
// Process them and create/update local calendar items.
|
||||
|
||||
foreach (var @event in allEvents)
|
||||
{
|
||||
// TODO: Exception handling for event processing.
|
||||
// TODO: Also update attendees and other properties.
|
||||
|
||||
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
|
||||
Console.WriteLine($"Stored sync token for calendar {calendarId} to enable delta sync");
|
||||
}
|
||||
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to synchronize calendar {calendarId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
private async Task<bool> DeltaSynchronizeCalendarAsync(AccountCalendar calendar)
|
||||
{
|
||||
var calendarId = calendar.RemoteCalendarId;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
@@ -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();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
1124
Wino.Services/CalendarServiceEx.cs
Normal file
1124
Wino.Services/CalendarServiceEx.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user