Event details UI improvements.
This commit is contained in:
@@ -817,15 +817,10 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
|
||||
private IEnumerable<CalendarItemViewModel> 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<CalendarItemViewModel>()
|
||||
.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<CalendarItemViewModel>()
|
||||
.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();
|
||||
|
||||
|
||||
@@ -18,54 +18,51 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
|
||||
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the event is part of a recurring series (as a child occurrence).
|
||||
/// Used to enable "View Series" functionality.
|
||||
/// </summary>
|
||||
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false;
|
||||
|
||||
#endregion
|
||||
|
||||
public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
|
||||
#region Show As Options
|
||||
|
||||
public List<CalendarItemShowAs> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ public class CalendarItem : ICalendarItem
|
||||
public string Description { get; set; }
|
||||
public string Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId);
|
||||
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
public DateTime EndDate
|
||||
@@ -187,66 +195,70 @@ public class CalendarItem : ICalendarItem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Defines how a calendar item should be displayed in terms of availability.
|
||||
/// </summary>
|
||||
public enum CalendarItemShowAs
|
||||
{
|
||||
Free,
|
||||
Tentative,
|
||||
Busy,
|
||||
OutOfOffice,
|
||||
WorkingElsewhere
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface ICalendarDialogService : IDialogServiceBase
|
||||
{
|
||||
}
|
||||
@@ -11,9 +11,3 @@ namespace Wino.Core.Domain.Models.Calendar;
|
||||
/// <param name="CalendarItem">Calendar item to operate on.</param>
|
||||
/// <param name="Attendees">List of attendees for the calendar event.</param>
|
||||
public record CalendarOperationPreparationRequest(CalendarSynchronizerOperation Operation, CalendarItem CalendarItem, List<CalendarEventAttendee> Attendees);
|
||||
//{
|
||||
// public CalendarOperationPreparationRequest(CalendarItem calendarItem)
|
||||
// : this(calendarItem ?? throw new ArgumentNullException(nameof(calendarItem)), null)
|
||||
// {
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -170,7 +170,9 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
List<CalendarEventAttendee> 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Request to delete a calendar event on the server.
|
||||
/// </summary>
|
||||
public record DeleteCalendarEventRequest(CalendarItem Item) : CalendarRequestBase(Item)
|
||||
{
|
||||
public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeleteEvent;
|
||||
|
||||
/// <summary>
|
||||
/// After successful deletion, resync to confirm the event was removed.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the calendar item has an online meeting link.
|
||||
/// </summary>
|
||||
public static bool HasOnlineMeetingLink(CalendarItemViewModel calendarItemViewModel)
|
||||
=> calendarItemViewModel != null && !string.IsNullOrEmpty(calendarItemViewModel.CalendarItem?.HtmlLink);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<AppBarToggleButton
|
||||
x:Name="ReadOnlyToggle"
|
||||
Content="Read-only event"
|
||||
Content="{x:Bind domain:Translator.CalendarEventDetails_ReadOnlyEvent}"
|
||||
IsChecked="True" />
|
||||
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Save}">
|
||||
<AppBarButton Command="{x:Bind ViewModel.SaveCommand}" Label="{x:Bind domain:Translator.Buttons_Save}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="Save" />
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Delete}">
|
||||
<AppBarButton Command="{x:Bind ViewModel.DeleteCommand}" Label="{x:Bind domain:Translator.Buttons_Delete}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="Delete" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -77,7 +78,10 @@
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Join Online -->
|
||||
<AppBarButton Label="Join Online">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.JoinOnlineCommand}"
|
||||
IsEnabled="{x:Bind calendarHelpers:CalendarXamlHelpers.HasOnlineMeetingLink(ViewModel.CurrentEvent), Mode=OneWay}"
|
||||
Label="{x:Bind domain:Translator.CalendarEventDetails_JoinOnline}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="EventJoinOnline" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -85,27 +89,30 @@
|
||||
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Join Options -->
|
||||
<AppBarButton Label="Accept">
|
||||
<!-- Response Options -->
|
||||
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.RespondCommand}"
|
||||
CommandParameter="{x:Bind enums:CalendarItemStatus.Confirmed}"
|
||||
Label="{x:Bind domain:Translator.CalendarEventResponse_Accept}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Foreground="#527257" Icon="EventAccept" />
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
|
||||
<AppBarButton Label="Tentative">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.RespondCommand}"
|
||||
CommandParameter="{x:Bind enums:CalendarItemStatus.Tentative}"
|
||||
Label="{x:Bind domain:Translator.CalendarEventResponse_Tentative}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventTentative" />
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
|
||||
<AppBarButton Label="Decline">
|
||||
<AppBarButton.Icon>
|
||||
<PathIcon Data="F1 M 10.253906 9.375 L 16.064453 15.185547 C 16.18815 15.309245 16.25 15.455729 16.25 15.625 C 16.25 15.794271 16.18815 15.940756 16.064453 16.064453 C 15.940754 16.188152 15.79427 16.25 15.625 16.25 C 15.455729 16.25 15.309244 16.188152 15.185547 16.064453 L 9.375 10.253906 L 3.564453 16.064453 C 3.440755 16.188152 3.294271 16.25 3.125 16.25 C 2.955729 16.25 2.809245 16.188152 2.685547 16.064453 C 2.561849 15.940756 2.5 15.794271 2.5 15.625 C 2.5 15.455729 2.561849 15.309245 2.685547 15.185547 L 8.496094 9.375 L 2.685547 3.564453 C 2.561849 3.440756 2.5 3.294271 2.5 3.125 C 2.5 2.95573 2.561849 2.809246 2.685547 2.685547 C 2.809245 2.56185 2.955729 2.5 3.125 2.5 C 3.294271 2.5 3.440755 2.56185 3.564453 2.685547 L 9.375 8.496094 L 15.185547 2.685547 C 15.309244 2.56185 15.455729 2.5 15.625 2.5 C 15.79427 2.5 15.940754 2.56185 16.064453 2.685547 C 16.18815 2.809246 16.25 2.95573 16.25 3.125 C 16.25 3.294271 16.18815 3.440756 16.064453 3.564453 Z " Foreground="#d94b4e" />
|
||||
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
|
||||
<AppBarButton Label="Respond">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.RespondCommand}"
|
||||
CommandParameter="{x:Bind enums:CalendarItemStatus.Cancelled}"
|
||||
Label="{x:Bind domain:Translator.CalendarEventResponse_Decline}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventRespond" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -116,8 +123,11 @@
|
||||
<!-- Show as -->
|
||||
<AppBarElementContainer>
|
||||
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
||||
<TextBlock VerticalAlignment="Center" Text="Show as" />
|
||||
<ComboBox Width="150" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_ShowAs}" />
|
||||
<ComboBox
|
||||
Width="150"
|
||||
ItemsSource="{x:Bind ViewModel.ShowAsOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedShowAs, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</AppBarElementContainer>
|
||||
|
||||
@@ -125,7 +135,7 @@
|
||||
<AppBarElementContainer>
|
||||
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
||||
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
||||
<TextBlock VerticalAlignment="Center" Text="Reminder" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_Reminder}" />
|
||||
<ComboBox Width="150" />
|
||||
</StackPanel>
|
||||
</AppBarElementContainer>
|
||||
@@ -133,7 +143,7 @@
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Edit Series -->
|
||||
<AppBarButton Label="Edit Series">
|
||||
<AppBarButton Label="{x:Bind domain:Translator.CalendarEventDetails_EditSeries}" Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="EventEditSeries" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -163,7 +173,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="Details" />
|
||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_Details}" />
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<!-- Read-Only Event -->
|
||||
@@ -194,7 +204,7 @@
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
ColumnSpacing="6"
|
||||
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringEvent}">
|
||||
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringParent}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -212,7 +222,7 @@
|
||||
|
||||
<!-- Editable Event -->
|
||||
<Grid Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(ReadOnlyDetailsGrid.Visibility), Mode=OneWay}">
|
||||
<TextBlock Text="editing" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.CalendarEventDetails_Editing}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -227,7 +237,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="People" />
|
||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_People}" />
|
||||
|
||||
<Grid Grid.Row="1" RowSpacing="12">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -237,7 +247,7 @@
|
||||
<AutoSuggestBox
|
||||
Margin="6,0"
|
||||
BorderThickness="0"
|
||||
PlaceholderText="Invite someone" />
|
||||
PlaceholderText="{x:Bind domain:Translator.CalendarEventDetails_InviteSomeone}" />
|
||||
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
@@ -287,7 +297,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="Attachments" />
|
||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_Attachments}" />
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user