From 3b485dc1feaf9124acf4aba05a9e67a9911cf53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 1 Jan 2026 10:07:56 +0100 Subject: [PATCH] Event details UI improvements. --- .../CalendarPageViewModel.cs | 64 +++++++++++-- .../Data/CalendarItemViewModel.cs | 33 +++---- .../EventDetailsPageViewModel.cs | 81 +++++++++++++++- .../Entities/Calendar/CalendarItem.cs | 92 +++++++++++-------- Wino.Core.Domain/Enums/CalendarItemShowAs.cs | 13 +++ .../Interfaces/ICalendarDialogService.cs | 5 - .../CalendarOperationPreparationRequest.cs | 6 -- .../Translations/en_US/resources.json | 15 +++ .../Extensions/OutlookIntegratorExtensions.cs | 7 +- .../Processors/OutlookChangeProcessor.cs | 4 +- .../Calendar/DeleteCalendarEventRequest.cs | 32 +++++++ Wino.Core/Services/WinoRequestDelegator.cs | 4 +- .../Helpers/CalendarXamlHelpers.cs | 8 +- .../Views/Calendar/EventDetailsPage.xaml | 60 +++++++----- 14 files changed, 311 insertions(+), 113 deletions(-) create mode 100644 Wino.Core.Domain/Enums/CalendarItemShowAs.cs delete mode 100644 Wino.Core.Domain/Interfaces/ICalendarDialogService.cs create mode 100644 Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index cafa51eb..37a17e23 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -817,15 +817,10 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, private IEnumerable GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay) { - // All-day and multi-day events are selected collectively. - // 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. + // Multi-day events, all-day events, and recurring events are rendered across multiple days. + // We need to find all instances with the same ID across all visible date ranges. - if (!calendarItemViewModel.IsRecurringEvent) - { - return [calendarItemViewModel]; - } - else + if (calendarItemViewModel.IsRecurringEvent || calendarItemViewModel.IsMultiDayEvent) { return DayRanges .SelectMany(a => a.CalendarDays) @@ -834,6 +829,11 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, .Cast() .Distinct(); } + else + { + // Single-day, non-recurring events only appear once + return [calendarItemViewModel]; + } } private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null) @@ -877,6 +877,13 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, Debug.WriteLine($"Calendar item deleted: {calendarItem.Id}"); + // Check if the deleted item is currently displayed in details view + if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id) + { + // Clear the details view since this item was deleted + DisplayDetailsCalendarItemViewModel = null; + } + // Remove the event and its occurrences from all visible date ranges. await ExecuteUIThread(() => { @@ -956,6 +963,47 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, // Check if event falls into the current date range. if (DayRanges.DisplayRange == null) return; + // Check if this is a server-synced item that matches a local preview + // Local previews don't have RemoteEventId, server-synced items do + if (!string.IsNullOrEmpty(calendarItem.RemoteEventId)) + { + // Find local preview items that match this event's properties + var localPreviewItems = DayRanges + .SelectMany(a => a.CalendarDays) + .SelectMany(b => b.EventsCollection.RegularEvents.Concat(b.EventsCollection.AllDayEvents)) + .OfType() + .Where(c => c.AssignedCalendar.Id == calendarItem.CalendarId && + c.CalendarItem.IsLocalPreview && // Local preview (no RemoteEventId) + c.Title == calendarItem.Title && + Math.Abs((c.StartDate - calendarItem.LocalStartDate).TotalSeconds) < 60 && + Math.Abs(c.DurationInSeconds - calendarItem.DurationInSeconds) < 1) + .ToList(); + + if (localPreviewItems.Any()) + { + Debug.WriteLine($"Found {localPreviewItems.Count} matching local preview items for {calendarItem.Title}, removing them."); + + // Remove all matching local preview items + await ExecuteUIThread(() => + { + foreach (var dayRange in DayRanges) + { + foreach (var calendarDay in dayRange.CalendarDays) + { + foreach (var localPreview in localPreviewItems) + { + var itemInDay = calendarDay.EventsCollection.GetCalendarItem(localPreview.Id); + if (itemInDay != null) + { + calendarDay.EventsCollection.RemoveCalendarItem(itemInDay); + } + } + } + } + }); + } + } + // Get all periods from the visible day ranges var visiblePeriods = DayRanges.Select(dr => dr.Period).ToList(); diff --git a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs index 1af336ca..3508f968 100644 --- a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs @@ -18,54 +18,51 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar; /// - /// Gets or sets the start date in local time based on the event's timezone. - /// The underlying CalendarItem stores dates in UTC. + /// Gets or sets the start date converted to user's local timezone for display. + /// The underlying CalendarItem stores dates according to their timezone. /// public DateTime StartDate { get { - // Convert from UTC stored in database to local time using the event's timezone - var startDateTimeOffset = CalendarItem.StartDateTimeOffset; - return startDateTimeOffset.LocalDateTime; + // Get start date in user's local timezone + return CalendarItem.LocalStartDate; } set { - // When setting, convert from local time to UTC for storage - // Preserve the timezone information + // When setting from UI (in local time), convert to event's timezone for storage if (!string.IsNullOrEmpty(CalendarItem.StartTimeZone)) { try { - var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.StartTimeZone); - var utcDateTime = TimeZoneInfo.ConvertTimeToUtc(value, timeZoneInfo); - CalendarItem.StartDate = utcDateTime; + var sourceTimeZone = TimeZoneInfo.Local; + var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.StartTimeZone); + CalendarItem.StartDate = TimeZoneInfo.ConvertTime(value, sourceTimeZone, targetTimeZone); } catch { - // If timezone lookup fails, assume value is already in UTC + // If timezone lookup fails, set as-is CalendarItem.StartDate = value; } } else { - // No timezone info, assume UTC + // No timezone info, set as-is CalendarItem.StartDate = value; } } } /// - /// Gets the end date in local time based on the event's timezone. - /// The underlying CalendarItem stores dates in UTC. + /// Gets the end date converted to user's local timezone for display. + /// The underlying CalendarItem stores dates according to their timezone. /// public DateTime EndDate { get { - // Convert from UTC stored in database to local time using the event's timezone - var endDateTimeOffset = CalendarItem.EndDateTimeOffset; - return endDateTimeOffset.LocalDateTime; + // Get end date in user's local timezone + return CalendarItem.LocalEndDate; } } @@ -100,4 +97,4 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC } public override string ToString() => CalendarItem.Title; -} \ No newline at end of file +} diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs index f57d797e..4230e8ef 100644 --- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Calendar; @@ -19,6 +21,9 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel private readonly ICalendarService _calendarService; private readonly INativeAppService _nativeAppService; private readonly IPreferencesService _preferencesService; + private readonly IMailDialogService _dialogService; + private readonly IWinoRequestDelegator _winoRequestDelegator; + private readonly INavigationService _navigationService; public CalendarSettings CurrentSettings { get; } @@ -26,20 +31,55 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanViewSeries))] + [NotifyPropertyChangedFor(nameof(CanEditSeries))] private CalendarItemViewModel _currentEvent; [ObservableProperty] private CalendarItemViewModel _seriesParent; + /// + /// Returns true if the event is part of a recurring series (as a child occurrence). + /// Used to enable "View Series" functionality. + /// public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false; + /// + /// Returns true if the "Edit Series" button should be visible. + /// Only visible for child occurrences of recurring events, not for master events or single events. + /// + public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false; + #endregion - public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService) + #region Show As Options + + public List ShowAsOptions { get; } = + [ + CalendarItemShowAs.Free, + CalendarItemShowAs.Tentative, + CalendarItemShowAs.Busy, + CalendarItemShowAs.OutOfOffice, + CalendarItemShowAs.WorkingElsewhere + ]; + + [ObservableProperty] + public partial CalendarItemShowAs SelectedShowAs { get; set; } = CalendarItemShowAs.Busy; + + #endregion + + public EventDetailsPageViewModel(ICalendarService calendarService, + INativeAppService nativeAppService, + IPreferencesService preferencesService, + IMailDialogService dialogService, + IWinoRequestDelegator winoRequestDelegator, + INavigationService navigationService) { _calendarService = calendarService; _nativeAppService = nativeAppService; _preferencesService = preferencesService; + _dialogService = dialogService; + _winoRequestDelegator = winoRequestDelegator; + _navigationService = navigationService; CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); } @@ -90,26 +130,57 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel [RelayCommand] private async Task SaveAsync() { - + // TODO: Implement saving } [RelayCommand] private async Task DeleteAsync() { + if (CurrentEvent == null) return; + // If the event is a master recurring event, ask for confirmation + if (CurrentEvent.IsRecurringParent) + { + var confirmed = await _dialogService.ShowConfirmationDialogAsync( + Translator.DialogMessage_DeleteRecurringSeriesMessage, + Translator.DialogMessage_DeleteRecurringSeriesTitle, + Translator.Buttons_Delete); + + if (!confirmed) return; + } + + try + { + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.DeleteEvent, + CurrentEvent.CalendarItem, + null); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest); + + // Navigate back after successful deletion + _navigationService.GoBack(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error deleting calendar event: {ex.Message}"); + } } [RelayCommand] - private Task JoinOnline() + private Task JoinOnlineAsync() { - if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask; + if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) + return Task.CompletedTask; return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink)); } [RelayCommand] - private async Task Respond(CalendarItemStatus status) + private async Task RespondAsync(CalendarItemStatus status) { if (CurrentEvent == null) return; + + // TODO: Implement response } } diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs index ab47d74e..20194b8a 100644 --- a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs +++ b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs @@ -17,6 +17,14 @@ public class CalendarItem : ICalendarItem public string Description { get; set; } public string Location { get; set; } + /// + /// Indicates whether this item is a local preview that hasn't been synced to the server yet. + /// When true, the item exists only in the local database without a RemoteEventId. + /// Used to prevent duplicates when the server returns the newly created event. + /// + [Ignore] + public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId); + public DateTime StartDate { get; set; } public DateTime EndDate @@ -187,66 +195,70 @@ public class CalendarItem : ICalendarItem } /// - /// Gets the start date as a DateTimeOffset with the correct timezone. - /// If StartTimeZone is available, uses it to calculate the offset. - /// Otherwise, assumes UTC (StartDate is stored as UTC in database). + /// Gets the start date converted to user's local timezone for display. + /// StartDate is stored according to StartTimeZone. /// [Ignore] - public DateTimeOffset StartDateTimeOffset + public DateTime LocalStartDate { get { - if (!string.IsNullOrEmpty(StartTimeZone)) + if (string.IsNullOrEmpty(StartTimeZone)) { - try - { - var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(StartTimeZone); - // StartDate is stored in UTC, convert to the specified timezone - var utcDateTime = DateTime.SpecifyKind(StartDate, DateTimeKind.Utc); - var zonedDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, timeZoneInfo); - var offset = timeZoneInfo.GetUtcOffset(zonedDateTime); - return new DateTimeOffset(zonedDateTime, offset); - } - catch - { - // If timezone lookup fails, assume UTC - } + // No timezone info, return as-is + return StartDate; } - // Assume UTC (StartDate is stored as UTC in database) - return new DateTimeOffset(StartDate, TimeSpan.Zero); + try + { + var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(StartTimeZone); + var localTimeZone = TimeZoneInfo.Local; + + // Ensure DateTime is Unspecified kind before conversion + var unspecifiedDateTime = DateTime.SpecifyKind(StartDate, DateTimeKind.Unspecified); + + // Convert from source timezone to local timezone + return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, localTimeZone); + } + catch + { + // If timezone lookup fails, return as-is + return StartDate; + } } } /// - /// Gets the end date as a DateTimeOffset with the correct timezone. - /// If EndTimeZone is available, uses it to calculate the offset. - /// Otherwise, assumes UTC (EndDate is stored as UTC in database). + /// Gets the end date converted to user's local timezone for display. + /// EndDate is calculated from StartDate and is in StartTimeZone. /// [Ignore] - public DateTimeOffset EndDateTimeOffset + public DateTime LocalEndDate { get { - if (!string.IsNullOrEmpty(EndTimeZone)) + if (string.IsNullOrEmpty(EndTimeZone)) { - try - { - var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(EndTimeZone); - // EndDate is stored in UTC, convert to the specified timezone - var utcDateTime = DateTime.SpecifyKind(EndDate, DateTimeKind.Utc); - var zonedDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, timeZoneInfo); - var offset = timeZoneInfo.GetUtcOffset(zonedDateTime); - return new DateTimeOffset(zonedDateTime, offset); - } - catch - { - // If timezone lookup fails, assume UTC - } + // No timezone info, return as-is + return EndDate; } - // Assume UTC (EndDate is stored as UTC in database) - return new DateTimeOffset(EndDate, TimeSpan.Zero); + try + { + var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(EndTimeZone); + var localTimeZone = TimeZoneInfo.Local; + + // Ensure DateTime is Unspecified kind before conversion + var unspecifiedDateTime = DateTime.SpecifyKind(EndDate, DateTimeKind.Unspecified); + + // Convert from source timezone to local timezone + return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, localTimeZone); + } + catch + { + // If timezone lookup fails, return as-is + return EndDate; + } } } } diff --git a/Wino.Core.Domain/Enums/CalendarItemShowAs.cs b/Wino.Core.Domain/Enums/CalendarItemShowAs.cs new file mode 100644 index 00000000..57c55f00 --- /dev/null +++ b/Wino.Core.Domain/Enums/CalendarItemShowAs.cs @@ -0,0 +1,13 @@ +namespace Wino.Core.Domain.Enums; + +/// +/// Defines how a calendar item should be displayed in terms of availability. +/// +public enum CalendarItemShowAs +{ + Free, + Tentative, + Busy, + OutOfOffice, + WorkingElsewhere +} diff --git a/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs b/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs deleted file mode 100644 index 7b48f868..00000000 --- a/Wino.Core.Domain/Interfaces/ICalendarDialogService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface ICalendarDialogService : IDialogServiceBase -{ -} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs b/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs index 81a9c342..0705681e 100644 --- a/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs +++ b/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs @@ -11,9 +11,3 @@ namespace Wino.Core.Domain.Models.Calendar; /// Calendar item to operate on. /// List of attendees for the calendar event. public record CalendarOperationPreparationRequest(CalendarSynchronizerOperation Operation, CalendarItem CalendarItem, List Attendees); -//{ -// public CalendarOperationPreparationRequest(CalendarItem calendarItem) -// : this(calendarItem ?? throw new ArgumentNullException(nameof(calendarItem)), null) -// { -// } -//} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 2703df27..d7fa3316 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -83,6 +83,19 @@ "CalendarAllDayEventSummary": "all-day events", "CalendarDisplayOptions_Color": "Color", "CalendarDisplayOptions_Expand": "Expand", + "CalendarEventResponse_Accept": "Accept", + "CalendarEventResponse_Decline": "Decline", + "CalendarEventResponse_Tentative": "Tentative", + "CalendarEventDetails_Attachments": "Attachments", + "CalendarEventDetails_Details": "Details", + "CalendarEventDetails_EditSeries": "Edit Series", + "CalendarEventDetails_Editing": "Editing", + "CalendarEventDetails_InviteSomeone": "Invite someone", + "CalendarEventDetails_JoinOnline": "Join Online", + "CalendarEventDetails_People": "People", + "CalendarEventDetails_ReadOnlyEvent": "Read-only event", + "CalendarEventDetails_Reminder": "Reminder", + "CalendarEventDetails_ShowAs": "Show as", "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", @@ -133,6 +146,8 @@ "DialogMessage_CreateLinkedAccountTitle": "Account Link Name", "DialogMessage_DeleteAccountConfirmationMessage": "Delete {0}?", "DialogMessage_DeleteAccountConfirmationTitle": "All data associated with this account will be deleted from disk permanently.", + "DialogMessage_DeleteRecurringSeriesMessage": "This will delete all events in the series. Do you want to continue?", + "DialogMessage_DeleteRecurringSeriesTitle": "Delete Recurring Series", "DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?", "DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs index ab95ff0d..ccec151b 100644 --- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs +++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs @@ -283,9 +283,12 @@ public static class OutlookIntegratorExtensions }; } - public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId) + public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId, string organizerEmail = null) { - bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer; + // Check if this attendee is the organizer by comparing email addresses + bool isOrganizer = !string.IsNullOrEmpty(organizerEmail) && + !string.IsNullOrEmpty(attendee?.EmailAddress?.Address) && + string.Equals(attendee.EmailAddress.Address, organizerEmail, StringComparison.OrdinalIgnoreCase); var eventAttendee = new CalendarEventAttendee() { diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs index 473fbb89..640c6cfa 100644 --- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs @@ -170,7 +170,9 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, List attendees = null; if (calendarEvent.Attendees != null) { - attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList(); + // Pass the organizer's email address to properly identify the organizer in the attendees list + string organizerEmail = calendarEvent.Organizer?.EmailAddress?.Address; + attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId, organizerEmail)).ToList(); } // Use CalendarService to create or update the event diff --git a/Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs b/Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs new file mode 100644 index 00000000..a9ae4681 --- /dev/null +++ b/Wino.Core/Requests/Calendar/DeleteCalendarEventRequest.cs @@ -0,0 +1,32 @@ +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.Client.Calendar; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to delete a calendar event on the server. +/// +public record DeleteCalendarEventRequest(CalendarItem Item) : CalendarRequestBase(Item) +{ + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeleteEvent; + + /// + /// After successful deletion, resync to confirm the event was removed. + /// + public override int ResynchronizationDelay => 2000; + + public override void ApplyUIChanges() + { + // Notify UI that the event was deleted + WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(Item)); + } + + public override void RevertUIChanges() + { + // If deletion fails, we should notify the UI to add it back + WeakReferenceMessenger.Default.Send(new CalendarItemAdded(Item)); + } +} diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index 1303ec8f..2edf164c 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -140,9 +140,9 @@ public class WinoRequestDelegator : IWinoRequestDelegator IRequestBase request = calendarPreparationRequest.Operation switch { CalendarSynchronizerOperation.CreateEvent => new CreateCalendarEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.Attendees), - // Future support for update and delete operations + CalendarSynchronizerOperation.DeleteEvent => new DeleteCalendarEventRequest(calendarPreparationRequest.CalendarItem), + // Future support for update operations // CalendarSynchronizerOperation.UpdateEvent => new UpdateCalendarEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.Attendees), - // CalendarSynchronizerOperation.DeleteEvent => new DeleteCalendarEventRequest(calendarPreparationRequest.CalendarItem), _ => throw new NotImplementedException($"Calendar operation {calendarPreparationRequest.Operation} is not implemented yet.") }; diff --git a/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs b/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs index 86267f58..dffe475d 100644 --- a/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs +++ b/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs @@ -42,7 +42,7 @@ public static class CalendarXamlHelpers public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel) { - if (calendarItemViewModel == null || !calendarItemViewModel.IsRecurringChild) return string.Empty; + if (calendarItemViewModel == null || string.IsNullOrEmpty(calendarItemViewModel.CalendarItem.Recurrence)) return string.Empty; // Parse recurrence rules var calendarEvent = new CalendarEvent @@ -103,4 +103,10 @@ public static class CalendarXamlHelpers return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType); } + + /// + /// Returns true if the calendar item has an online meeting link. + /// + public static bool HasOnlineMeetingLink(CalendarItemViewModel calendarItemViewModel) + => calendarItemViewModel != null && !string.IsNullOrEmpty(calendarItemViewModel.CalendarItem?.HtmlLink); } diff --git a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml index 7738d65f..14d2cdc6 100644 --- a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml @@ -9,6 +9,7 @@ xmlns:coreControls="using:Wino.Mail.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:domain="using:Wino.Core.Domain" + xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:helpers="using:Wino.Helpers" xmlns:local="using:Wino.Calendar.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -61,14 +62,14 @@ OverflowButtonVisibility="Auto"> - + - + @@ -77,7 +78,10 @@ - + @@ -85,27 +89,30 @@ - - + + + - + - - - - - - - - + @@ -116,8 +123,11 @@ - - + + @@ -125,7 +135,7 @@ - + @@ -133,7 +143,7 @@ - + @@ -163,7 +173,7 @@ - + @@ -194,7 +204,7 @@ + Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringParent}"> @@ -212,7 +222,7 @@ - + @@ -227,7 +237,7 @@ - + @@ -237,7 +247,7 @@ + PlaceholderText="{x:Bind domain:Translator.CalendarEventDetails_InviteSomeone}" /> - +