Refactored the calendar synchronization code using AI.

This commit is contained in:
Burak Kaan Köse
2025-07-06 17:25:38 +02:00
parent 3e889d8c08
commit 1ee0063b62
30 changed files with 3125 additions and 1086 deletions

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,29 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class CalendarEventAttendee
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public AttendeeStatus AttendenceStatus { get; set; }
public bool IsOrganizer { get; set; }
public bool IsOptionalAttendee { get; set; }
public string Comment { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
[NotNull]
public Guid EventId { get; set; }
[NotNull]
public string Email { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public AttendeeResponseStatus ResponseStatus { get; set; } = AttendeeResponseStatus.NeedsAction;
public bool IsOptional { get; set; } = false;
public bool IsOrganizer { get; set; } = false;
public bool IsSelf { get; set; } = false;
public string? Comment { get; set; }
public int? AdditionalGuests { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
}

View File

@@ -3,6 +3,7 @@ using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Helpers;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
@@ -11,169 +12,81 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class CalendarItem : ICalendarItem
{
[PrimaryKey]
public Guid Id { get; set; }
public string RemoteEventId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime StartDate { get; set; }
[NotNull]
public string RemoteEventId { get; set; } = string.Empty;
public DateTime EndDate
{
get
{
return StartDate.AddSeconds(DurationInSeconds);
}
}
[NotNull]
public Guid CalendarId { get; set; }
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
[Ignore]
public IAccountCalendar AssignedCalendar { get; set; }
[NotNull]
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Location { get; set; }
public string? HtmlLink { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
{
get
{
_period ??= new TimeRange(StartDate, EndDate);
_period ??= new TimeRange(StartDateTime, EndDateTime);
return _period;
}
}
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; } = CalendarItemType.Timed;
/// <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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}