diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 087e76d2..4703131a 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -35,6 +35,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, IRecipient, IRecipient, IRecipient, + IRecipient, IRecipient, IDisposable { @@ -229,6 +230,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); + Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Register(this); @@ -236,6 +238,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); + Messenger.Register(this); Messenger.Register(this); } @@ -248,6 +251,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); + Messenger.Unregister(this); Messenger.Unregister(this); } @@ -802,6 +806,24 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, public void Receive(CalendarItemRightTappedMessage message) { + if (message.CalendarItemViewModel == null) + return; + + DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel; + } + + public void Receive(CalendarItemContextActionRequestedMessage message) + { + if (message.CalendarItemViewModel == null) + return; + + if (message.Action.ActionType == CalendarContextMenuActionType.Open) + { + NavigateEvent(message.CalendarItemViewModel, message.Action.TargetType ?? CalendarEventTargetType.Single); + return; + } + + _ = ExecuteContextActionAsync(message.CalendarItemViewModel, message.Action); } public async void Receive(AccountRemovedMessage message) @@ -1122,6 +1144,127 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, return trackedLocalItemId.HasValue && pendingCalendarItemIds.Contains(trackedLocalItemId.Value); } + private async Task ExecuteContextActionAsync(CalendarItemViewModel calendarItemViewModel, CalendarContextMenuAction action) + { + switch (action.ActionType) + { + case CalendarContextMenuActionType.JoinOnline: + await JoinOnlineAsync(calendarItemViewModel).ConfigureAwait(false); + break; + case CalendarContextMenuActionType.Delete: + await DeleteCalendarItemAsync(calendarItemViewModel, action.TargetType ?? CalendarEventTargetType.Single).ConfigureAwait(false); + break; + case CalendarContextMenuActionType.ShowAs when action.ShowAs.HasValue: + await UpdateShowAsAsync(calendarItemViewModel, action.TargetType ?? CalendarEventTargetType.Single, action.ShowAs.Value).ConfigureAwait(false); + break; + case CalendarContextMenuActionType.Respond when action.ResponseStatus.HasValue: + await RespondToCalendarItemAsync(calendarItemViewModel, action.TargetType ?? CalendarEventTargetType.Single, action.ResponseStatus.Value).ConfigureAwait(false); + break; + } + } + + private Task JoinOnlineAsync(CalendarItemViewModel calendarItemViewModel) + { + var htmlLink = calendarItemViewModel?.CalendarItem?.HtmlLink; + + if (string.IsNullOrWhiteSpace(htmlLink)) + return Task.CompletedTask; + + return _nativeAppService.LaunchUriAsync(new Uri(htmlLink)); + } + + private async Task DeleteCalendarItemAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType) + { + var targetItem = await ResolveCalendarItemTargetAsync(calendarItemViewModel, targetType).ConfigureAwait(false); + + if (targetItem == null) + return; + + if (targetItem.IsRecurringParent) + { + var confirmed = await _dialogService.ShowConfirmationDialogAsync( + Translator.DialogMessage_DeleteRecurringSeriesMessage, + Translator.DialogMessage_DeleteRecurringSeriesTitle, + Translator.Buttons_Delete).ConfigureAwait(false); + + if (!confirmed) + return; + } + + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.DeleteEvent, + targetItem, + null); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false); + } + + private async Task UpdateShowAsAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType, CalendarItemShowAs showAs) + { + var targetItem = await ResolveCalendarItemTargetAsync(calendarItemViewModel, targetType).ConfigureAwait(false); + + if (targetItem == null || targetItem.ShowAs == showAs) + return; + + var originalItem = await _calendarService.GetCalendarItemAsync(targetItem.Id).ConfigureAwait(false); + var attendees = await _calendarService.GetAttendeesAsync(targetItem.Id).ConfigureAwait(false); + + targetItem.ShowAs = showAs; + await _calendarService.UpdateCalendarItemAsync(targetItem, attendees).ConfigureAwait(false); + + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.UpdateEvent, + targetItem, + attendees, + ResponseMessage: null, + OriginalItem: originalItem, + OriginalAttendees: attendees); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false); + } + + private async Task RespondToCalendarItemAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType, CalendarItemStatus responseStatus) + { + var targetItem = await ResolveCalendarItemTargetAsync(calendarItemViewModel, targetType).ConfigureAwait(false); + + if (targetItem == null) + return; + + var operation = responseStatus switch + { + CalendarItemStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent, + CalendarItemStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent, + CalendarItemStatus.Cancelled => CalendarSynchronizerOperation.DeclineEvent, + _ => throw new InvalidOperationException($"Unsupported calendar response status '{responseStatus}'.") + }; + + var preparationRequest = new CalendarOperationPreparationRequest( + operation, + targetItem, + null); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false); + } + + private async Task ResolveCalendarItemTargetAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType) + { + if (calendarItemViewModel?.CalendarItem == null) + return null; + + var target = new CalendarItemTarget(calendarItemViewModel.CalendarItem, targetType); + var targetItem = await _calendarService.GetCalendarItemTargetAsync(target).ConfigureAwait(false); + + targetItem ??= calendarItemViewModel.CalendarItem; + if (targetItem == calendarItemViewModel.CalendarItem || targetItem.AssignedCalendar == null) + { + targetItem.AssignedCalendar = await _calendarService.GetAccountCalendarAsync(targetItem.CalendarId).ConfigureAwait(false) + ?? calendarItemViewModel.AssignedCalendar + ?? ResolveAssignedCalendar(targetItem.CalendarId); + } + + return targetItem; + } + private AccountCalendarViewModel ResolveAssignedCalendar(Guid calendarId) => AccountCalendarStateService.AllCalendars.FirstOrDefault(calendar => calendar.Id == calendarId); diff --git a/Wino.Calendar.ViewModels/Messages/CalendarItemContextActionRequestedMessage.cs b/Wino.Calendar.ViewModels/Messages/CalendarItemContextActionRequestedMessage.cs new file mode 100644 index 00000000..a4955716 --- /dev/null +++ b/Wino.Calendar.ViewModels/Messages/CalendarItemContextActionRequestedMessage.cs @@ -0,0 +1,8 @@ +using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Calendar.ViewModels.Messages; + +public sealed record CalendarItemContextActionRequestedMessage( + CalendarItemViewModel CalendarItemViewModel, + CalendarContextMenuAction Action); diff --git a/Wino.Core.Domain/Enums/CalendarContextMenuActionType.cs b/Wino.Core.Domain/Enums/CalendarContextMenuActionType.cs new file mode 100644 index 00000000..4a585853 --- /dev/null +++ b/Wino.Core.Domain/Enums/CalendarContextMenuActionType.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain.Enums; + +public enum CalendarContextMenuActionType +{ + Open, + JoinOnline, + Delete, + ShowAs, + Respond +} diff --git a/Wino.Core.Domain/Interfaces/ICalendarContextMenuItemService.cs b/Wino.Core.Domain/Interfaces/ICalendarContextMenuItemService.cs new file mode 100644 index 00000000..df13c155 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ICalendarContextMenuItemService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Core.Domain.Interfaces; + +public interface ICalendarContextMenuItemService +{ + IReadOnlyList GetContextMenuItems(CalendarItem calendarItem); +} diff --git a/Wino.Core.Domain/Models/Calendar/CalendarContextMenuAction.cs b/Wino.Core.Domain/Models/Calendar/CalendarContextMenuAction.cs new file mode 100644 index 00000000..f607940c --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarContextMenuAction.cs @@ -0,0 +1,9 @@ +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed record CalendarContextMenuAction( + CalendarContextMenuActionType ActionType, + CalendarEventTargetType? TargetType = null, + CalendarItemShowAs? ShowAs = null, + CalendarItemStatus? ResponseStatus = null); diff --git a/Wino.Core.Domain/Models/Calendar/CalendarContextMenuItem.cs b/Wino.Core.Domain/Models/Calendar/CalendarContextMenuItem.cs new file mode 100644 index 00000000..cddce556 --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/CalendarContextMenuItem.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed record CalendarContextMenuItem( + CalendarContextMenuAction Action, + bool IsPrimary = false, + bool IsEnabled = true, + IReadOnlyList ChildItems = null) +{ + public IReadOnlyList Children { get; init; } = ChildItems ?? []; + + public bool HasChildren => Children.Count > 0; +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 60b34cb6..d49740b2 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -192,6 +192,7 @@ "CalendarEventDetails_Organizer": "Organizer", "CalendarEventDetails_People": "People", "CalendarEventDetails_ReadOnlyEvent": "Read-only event", + "CalendarContextMenu_Respond": "Respond", "CalendarEventDetails_Reminder": "Reminder", "CalendarReminder_StartedHoursAgo": "Started {0} hours ago", "CalendarReminder_StartedMinutesAgo": "Started {0} minutes ago", @@ -207,7 +208,9 @@ "CalendarShowAs_Busy": "Busy", "CalendarShowAs_OutOfOffice": "Out of Office", "CalendarShowAs_WorkingElsewhere": "Working Elsewhere", + "CalendarContextMenu_AllEventsInSeries": "All events in the series", "CalendarItem_DetailsPopup_JoinOnline": "Join online", + "CalendarContextMenu_ThisEventOnly": "This event only", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", "CalendarDragDropMoveNotAllowedMessage": "Only events you own and can edit can be moved.", diff --git a/Wino.Core.Tests/CalendarPageViewModelTests.cs b/Wino.Core.Tests/CalendarPageViewModelTests.cs index 63ff4faa..8e437cc1 100644 --- a/Wino.Core.Tests/CalendarPageViewModelTests.cs +++ b/Wino.Core.Tests/CalendarPageViewModelTests.cs @@ -18,6 +18,7 @@ using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Navigation; using Wino.Messaging.Client.Calendar; using Xunit; +using Wino.Calendar.ViewModels.Messages; namespace Wino.Core.Tests; @@ -311,6 +312,22 @@ public class CalendarPageViewModelTests accountCalendarViewModel.MailAccount.Should().BeSameAs(account); } + [Fact] + public void ReceiveCalendarItemRightTappedMessage_SelectsItemForDetails() + { + var settings = CreateSettings(); + var preferencesService = CreatePreferencesService(settings); + var calendarService = new Mock(); + var calendar = CreateCalendar(CreateAccount(), "Calendar"); + var itemViewModel = new CalendarItemViewModel(CreateCalendarItem(calendar.Id, new DateTime(2026, 3, 20, 9, 0, 0), "Tapped")); + + var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20)); + + viewModel.Receive(new CalendarItemRightTappedMessage(itemViewModel)); + + viewModel.DisplayDetailsCalendarItemViewModel.Should().BeSameAs(itemViewModel); + } + private static CalendarPageViewModel CreateViewModel( ICalendarService calendarService, IPreferencesService preferencesService, @@ -330,6 +347,17 @@ public class CalendarPageViewModelTests IPreferencesService preferencesService, DateOnly today, IAccountCalendarStateService accountCalendarStateService) + => CreateViewModel(calendarService, preferencesService, today, accountCalendarStateService, navigationService: Mock.Of()); + + private static CalendarPageViewModel CreateViewModel( + ICalendarService calendarService, + IPreferencesService preferencesService, + DateOnly today, + IAccountCalendarStateService accountCalendarStateService, + INavigationService? navigationService = null, + INativeAppService? nativeAppService = null, + IWinoRequestDelegator? requestDelegator = null, + IMailDialogService? dialogService = null) { var statePersistenceService = new Mock(); statePersistenceService.SetupAllProperties(); @@ -339,13 +367,13 @@ public class CalendarPageViewModelTests return new CalendarPageViewModel( statePersistenceService.Object, calendarService, - Mock.Of(), + navigationService ?? Mock.Of(), Mock.Of(), - Mock.Of(), + nativeAppService ?? Mock.Of(), accountCalendarStateService, preferencesService, - Mock.Of(), - Mock.Of(), + requestDelegator ?? Mock.Of(), + dialogService ?? Mock.Of(), new TestDateContextProvider("en-US", today), new CalendarRangeTextFormatter()); } diff --git a/Wino.Core.Tests/Services/CalendarContextMenuItemServiceTests.cs b/Wino.Core.Tests/Services/CalendarContextMenuItemServiceTests.cs new file mode 100644 index 00000000..e1a7b4f4 --- /dev/null +++ b/Wino.Core.Tests/Services/CalendarContextMenuItemServiceTests.cs @@ -0,0 +1,72 @@ +using System.Linq; +using FluentAssertions; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class CalendarContextMenuItemServiceTests +{ + private readonly CalendarContextMenuItemService _service = new(); + + [Fact] + public void GetContextMenuItems_ForEditableSingleEvent_ReturnsOpenShowAsAndDeleteAsPrimary() + { + var calendarItem = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Editable single event", + ShowAs = CalendarItemShowAs.Busy + }; + + var items = _service.GetContextMenuItems(calendarItem); + + items.Should().HaveCount(3); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.Open && item.IsPrimary); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.ShowAs && item.IsPrimary); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.Delete && item.IsPrimary); + items.Should().NotContain(item => item.Action.ActionType == CalendarContextMenuActionType.Respond); + + var showAsItem = items.Single(item => item.Action.ActionType == CalendarContextMenuActionType.ShowAs); + showAsItem.Children.Should().HaveCount(5); + showAsItem.Children.Select(child => child.Action.ShowAs).Should().BeEquivalentTo( + [ + CalendarItemShowAs.Free, + CalendarItemShowAs.Tentative, + CalendarItemShowAs.Busy, + CalendarItemShowAs.OutOfOffice, + CalendarItemShowAs.WorkingElsewhere + ]); + } + + [Fact] + public void GetContextMenuItems_ForLockedRecurringChild_ReturnsRespondDeleteViewSeriesAndJoinOnline() + { + var calendarItem = new CalendarItem + { + Id = Guid.NewGuid(), + Title = "Recurring invite", + IsLocked = true, + RecurringCalendarItemId = Guid.NewGuid(), + HtmlLink = "https://contoso.example/meeting" + }; + + var items = _service.GetContextMenuItems(calendarItem); + + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.Open && item.IsPrimary); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.Respond && item.IsPrimary); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.Delete && item.IsPrimary); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.Open && item.Action.TargetType == CalendarEventTargetType.Series && !item.IsPrimary); + items.Should().ContainSingle(item => item.Action.ActionType == CalendarContextMenuActionType.JoinOnline && !item.IsPrimary); + + var respondItem = items.Single(item => item.Action.ActionType == CalendarContextMenuActionType.Respond); + respondItem.Children.Should().HaveCount(2); + respondItem.Children.Select(child => child.Action.TargetType).Should().BeEquivalentTo([CalendarEventTargetType.Single, CalendarEventTargetType.Series]); + respondItem.Children.Should().OnlyContain(child => child.Children.Count == 3); + + var deleteItem = items.Single(item => item.Action.ActionType == CalendarContextMenuActionType.Delete); + deleteItem.Children.Select(child => child.Action.TargetType).Should().BeEquivalentTo([CalendarEventTargetType.Single, CalendarEventTargetType.Series]); + } +} diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemCommandBarFlyout.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemCommandBarFlyout.cs index 46146559..91bd2a44 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemCommandBarFlyout.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemCommandBarFlyout.cs @@ -1,11 +1,23 @@ -using Microsoft.UI.Xaml; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Wino.Calendar.ViewModels.Messages; 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; +using Wino.Mail.WinUI; +using Wino.Mail.WinUI.Controls; namespace Wino.Calendar.Controls; public partial class CalendarItemCommandBarFlyout : CommandBarFlyout { + private readonly ICalendarContextMenuItemService _contextMenuItemService; + public static readonly DependencyProperty ItemProperty = DependencyProperty.Register(nameof(Item), typeof(CalendarItemViewModel), typeof(CalendarItemCommandBarFlyout), new PropertyMetadata(null, new PropertyChangedCallback(OnItemChanged))); public CalendarItemViewModel Item @@ -14,6 +26,11 @@ public partial class CalendarItemCommandBarFlyout : CommandBarFlyout set { SetValue(ItemProperty, value); } } + public CalendarItemCommandBarFlyout() + { + _contextMenuItemService = WinoApplication.Current.Services.GetRequiredService(); + Opening += FlyoutOpening; + } private static void OnItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { @@ -23,8 +40,148 @@ public partial class CalendarItemCommandBarFlyout : CommandBarFlyout } } + private void FlyoutOpening(object sender, object e) => UpdateMenuItems(); + private void UpdateMenuItems() { + PrimaryCommands.Clear(); + SecondaryCommands.Clear(); + if (Item?.CalendarItem == null) + return; + + var menuItems = _contextMenuItemService.GetContextMenuItems(Item.CalendarItem); + + foreach (var menuItem in menuItems) + { + var appBarButton = BuildAppBarButton(menuItem); + + if (menuItem.IsPrimary) + PrimaryCommands.Add(appBarButton); + else + SecondaryCommands.Add(appBarButton); + } } + + private AppBarButton BuildAppBarButton(CalendarContextMenuItem menuItem) + { + var button = new AppBarButton + { + Label = GetActionLabel(menuItem.Action), + IsEnabled = menuItem.IsEnabled, + Icon = new WinoFontIcon + { + Icon = GetActionIcon(menuItem.Action), + FontSize = 16 + } + }; + + if (menuItem.HasChildren) + { + var flyout = new MenuFlyout(); + PopulateMenuFlyoutItems(flyout.Items, menuItem.Children); + button.Flyout = flyout; + } + else + { + button.Click += (_, _) => ExecuteAction(menuItem.Action); + } + + return button; + } + + private void PopulateMenuFlyoutItems(IList items, IReadOnlyList menuItems) + { + foreach (var menuItem in menuItems) + { + if (menuItem.HasChildren) + { + var subItem = new MenuFlyoutSubItem + { + Text = GetActionLabel(menuItem.Action), + IsEnabled = menuItem.IsEnabled + }; + + PopulateMenuFlyoutItems(subItem.Items, menuItem.Children); + items.Add(subItem); + } + else + { + var flyoutItem = new MenuFlyoutItem + { + Text = GetActionLabel(menuItem.Action), + IsEnabled = menuItem.IsEnabled + }; + + flyoutItem.Click += (_, _) => ExecuteAction(menuItem.Action); + items.Add(flyoutItem); + } + } + } + + private void ExecuteAction(CalendarContextMenuAction action) + { + if (Item == null) + return; + + WeakReferenceMessenger.Default.Send(new CalendarItemContextActionRequestedMessage(Item, action)); + Hide(); + } + + private static string GetActionLabel(CalendarContextMenuAction action) + { + if (action.ShowAs.HasValue) + { + return action.ShowAs.Value switch + { + CalendarItemShowAs.Free => Translator.CalendarShowAs_Free, + CalendarItemShowAs.Tentative => Translator.CalendarShowAs_Tentative, + CalendarItemShowAs.Busy => Translator.CalendarShowAs_Busy, + CalendarItemShowAs.OutOfOffice => Translator.CalendarShowAs_OutOfOffice, + CalendarItemShowAs.WorkingElsewhere => Translator.CalendarShowAs_WorkingElsewhere, + _ => Translator.CalendarShowAs_Busy + }; + } + + if (action.ResponseStatus.HasValue) + { + return action.ResponseStatus.Value switch + { + CalendarItemStatus.Accepted => Translator.CalendarEventResponse_Accept, + CalendarItemStatus.Tentative => Translator.CalendarEventResponse_Tentative, + CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_Decline, + _ => Translator.CalendarEventResponse_Accept + }; + } + + if (action.TargetType.HasValue && action.ActionType is CalendarContextMenuActionType.Delete or CalendarContextMenuActionType.ShowAs or CalendarContextMenuActionType.Respond) + { + return action.TargetType == CalendarEventTargetType.Single + ? Translator.CalendarContextMenu_ThisEventOnly + : Translator.CalendarContextMenu_AllEventsInSeries; + } + + return action.ActionType switch + { + CalendarContextMenuActionType.Open when action.TargetType == CalendarEventTargetType.Series => Translator.CalendarItem_DetailsPopup_ViewSeriesButton, + CalendarContextMenuActionType.Open => Translator.Buttons_Open, + CalendarContextMenuActionType.JoinOnline => Translator.CalendarItem_DetailsPopup_JoinOnline, + CalendarContextMenuActionType.Delete => Translator.Buttons_Delete, + CalendarContextMenuActionType.ShowAs => Translator.CalendarEventDetails_ShowAs, + CalendarContextMenuActionType.Respond => Translator.CalendarContextMenu_Respond, + _ => Translator.Buttons_Open + }; + } + + private static WinoIconGlyph GetActionIcon(CalendarContextMenuAction action) + => action.ActionType switch + { + CalendarContextMenuActionType.Open when action.TargetType == CalendarEventTargetType.Series => WinoIconGlyph.EventEditSeries, + CalendarContextMenuActionType.Open => WinoIconGlyph.OpenInNewWindow, + CalendarContextMenuActionType.JoinOnline => WinoIconGlyph.EventJoinOnline, + CalendarContextMenuActionType.Delete => WinoIconGlyph.Delete, + CalendarContextMenuActionType.ShowAs => WinoIconGlyph.CalendarShowAs, + CalendarContextMenuActionType.Respond => WinoIconGlyph.EventRespond, + _ => WinoIconGlyph.More + }; } diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml index 485f625c..db937ab6 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml @@ -29,12 +29,9 @@ - - - - - - + diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs index 0a3a217d..ef3bda74 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs @@ -130,7 +130,14 @@ public sealed partial class CalendarItemControl : UserControl private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e) { - if (CalendarItem == null) return; + if (CalendarItem == null) + return; + + if (CalendarItem.IsBusy) + { + e.Handled = true; + return; + } WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem)); } diff --git a/Wino.Services/CalendarContextMenuItemService.cs b/Wino.Services/CalendarContextMenuItemService.cs new file mode 100644 index 00000000..4614079b --- /dev/null +++ b/Wino.Services/CalendarContextMenuItemService.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Calendar; + +namespace Wino.Services; + +public class CalendarContextMenuItemService : ICalendarContextMenuItemService +{ + private static readonly IReadOnlyList ShowAsOptions = + [ + CalendarItemShowAs.Free, + CalendarItemShowAs.Tentative, + CalendarItemShowAs.Busy, + CalendarItemShowAs.OutOfOffice, + CalendarItemShowAs.WorkingElsewhere + ]; + + private static readonly IReadOnlyList ResponseOptions = + [ + CalendarItemStatus.Accepted, + CalendarItemStatus.Tentative, + CalendarItemStatus.Cancelled + ]; + + public IReadOnlyList GetContextMenuItems(CalendarItem calendarItem) + { + if (calendarItem == null) + return []; + + var items = new List + { + new(new CalendarContextMenuAction(CalendarContextMenuActionType.Open, CalendarEventTargetType.Single), IsPrimary: true) + }; + + if (calendarItem.IsLocked) + { + items.Add(CreateRespondItem(calendarItem.IsRecurringChild)); + } + else + { + items.Add(CreateShowAsItem(calendarItem.IsRecurringChild)); + } + + items.Add(CreateDeleteItem(calendarItem.IsRecurringChild)); + + if (calendarItem.IsRecurringChild) + { + items.Add(new CalendarContextMenuItem( + new CalendarContextMenuAction(CalendarContextMenuActionType.Open, CalendarEventTargetType.Series))); + } + + if (!string.IsNullOrWhiteSpace(calendarItem.HtmlLink)) + { + items.Add(new CalendarContextMenuItem( + new CalendarContextMenuAction(CalendarContextMenuActionType.JoinOnline))); + } + + return items; + } + + private static CalendarContextMenuItem CreateDeleteItem(bool isRecurringChild) + => isRecurringChild + ? new CalendarContextMenuItem( + new CalendarContextMenuAction(CalendarContextMenuActionType.Delete), + IsPrimary: true, + ChildItems: + [ + CreateScopeLeaf(CalendarContextMenuActionType.Delete, CalendarEventTargetType.Single), + CreateScopeLeaf(CalendarContextMenuActionType.Delete, CalendarEventTargetType.Series) + ]) + : new CalendarContextMenuItem( + new CalendarContextMenuAction(CalendarContextMenuActionType.Delete, CalendarEventTargetType.Single), + IsPrimary: true); + + private static CalendarContextMenuItem CreateShowAsItem(bool isRecurringChild) + => new( + new CalendarContextMenuAction(CalendarContextMenuActionType.ShowAs), + IsPrimary: true, + ChildItems: isRecurringChild + ? [CreateScopedShowAsMenu(CalendarEventTargetType.Single), CreateScopedShowAsMenu(CalendarEventTargetType.Series)] + : CreateShowAsLeaves(CalendarEventTargetType.Single)); + + private static CalendarContextMenuItem CreateRespondItem(bool isRecurringChild) + => new( + new CalendarContextMenuAction(CalendarContextMenuActionType.Respond), + IsPrimary: true, + ChildItems: isRecurringChild + ? [CreateScopedResponseMenu(CalendarEventTargetType.Single), CreateScopedResponseMenu(CalendarEventTargetType.Series)] + : CreateResponseLeaves(CalendarEventTargetType.Single)); + + private static CalendarContextMenuItem CreateScopedShowAsMenu(CalendarEventTargetType targetType) + => new( + new CalendarContextMenuAction(CalendarContextMenuActionType.ShowAs, targetType), + ChildItems: CreateShowAsLeaves(targetType)); + + private static CalendarContextMenuItem CreateScopedResponseMenu(CalendarEventTargetType targetType) + => new( + new CalendarContextMenuAction(CalendarContextMenuActionType.Respond, targetType), + ChildItems: CreateResponseLeaves(targetType)); + + private static IReadOnlyList CreateShowAsLeaves(CalendarEventTargetType targetType) + { + var items = new List(ShowAsOptions.Count); + + foreach (var showAs in ShowAsOptions) + { + items.Add(new CalendarContextMenuItem( + new CalendarContextMenuAction(CalendarContextMenuActionType.ShowAs, targetType, showAs))); + } + + return items; + } + + private static IReadOnlyList CreateResponseLeaves(CalendarEventTargetType targetType) + { + var items = new List(ResponseOptions.Count); + + foreach (var responseStatus in ResponseOptions) + { + items.Add(new CalendarContextMenuItem( + new CalendarContextMenuAction(CalendarContextMenuActionType.Respond, targetType, ResponseStatus: responseStatus))); + } + + return items; + } + + private static CalendarContextMenuItem CreateScopeLeaf(CalendarContextMenuActionType actionType, CalendarEventTargetType targetType) + => new(new CalendarContextMenuAction(actionType, targetType)); +} diff --git a/Wino.Services/ServicesContainerSetup.cs b/Wino.Services/ServicesContainerSetup.cs index 5c2668b1..96db250d 100644 --- a/Wino.Services/ServicesContainerSetup.cs +++ b/Wino.Services/ServicesContainerSetup.cs @@ -25,6 +25,7 @@ public static class ServicesContainerSetup services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddSingleton();