Calendar item context flyout implementation
This commit is contained in:
@@ -35,6 +35,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
IRecipient<CalendarItemTappedMessage>,
|
IRecipient<CalendarItemTappedMessage>,
|
||||||
IRecipient<CalendarItemDoubleTappedMessage>,
|
IRecipient<CalendarItemDoubleTappedMessage>,
|
||||||
IRecipient<CalendarItemRightTappedMessage>,
|
IRecipient<CalendarItemRightTappedMessage>,
|
||||||
|
IRecipient<CalendarItemContextActionRequestedMessage>,
|
||||||
IRecipient<AccountRemovedMessage>,
|
IRecipient<AccountRemovedMessage>,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
@@ -229,6 +230,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
Messenger.Unregister<CalendarItemTappedMessage>(this);
|
Messenger.Unregister<CalendarItemTappedMessage>(this);
|
||||||
Messenger.Unregister<CalendarItemDoubleTappedMessage>(this);
|
Messenger.Unregister<CalendarItemDoubleTappedMessage>(this);
|
||||||
Messenger.Unregister<CalendarItemRightTappedMessage>(this);
|
Messenger.Unregister<CalendarItemRightTappedMessage>(this);
|
||||||
|
Messenger.Unregister<CalendarItemContextActionRequestedMessage>(this);
|
||||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||||
|
|
||||||
Messenger.Register<LoadCalendarMessage>(this);
|
Messenger.Register<LoadCalendarMessage>(this);
|
||||||
@@ -236,6 +238,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
Messenger.Register<CalendarItemTappedMessage>(this);
|
Messenger.Register<CalendarItemTappedMessage>(this);
|
||||||
Messenger.Register<CalendarItemDoubleTappedMessage>(this);
|
Messenger.Register<CalendarItemDoubleTappedMessage>(this);
|
||||||
Messenger.Register<CalendarItemRightTappedMessage>(this);
|
Messenger.Register<CalendarItemRightTappedMessage>(this);
|
||||||
|
Messenger.Register<CalendarItemContextActionRequestedMessage>(this);
|
||||||
Messenger.Register<AccountRemovedMessage>(this);
|
Messenger.Register<AccountRemovedMessage>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +251,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
Messenger.Unregister<CalendarItemTappedMessage>(this);
|
Messenger.Unregister<CalendarItemTappedMessage>(this);
|
||||||
Messenger.Unregister<CalendarItemDoubleTappedMessage>(this);
|
Messenger.Unregister<CalendarItemDoubleTappedMessage>(this);
|
||||||
Messenger.Unregister<CalendarItemRightTappedMessage>(this);
|
Messenger.Unregister<CalendarItemRightTappedMessage>(this);
|
||||||
|
Messenger.Unregister<CalendarItemContextActionRequestedMessage>(this);
|
||||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,6 +806,24 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
public void Receive(CalendarItemRightTappedMessage message)
|
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)
|
public async void Receive(AccountRemovedMessage message)
|
||||||
@@ -1122,6 +1144,127 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
return trackedLocalItemId.HasValue && pendingCalendarItemIds.Contains(trackedLocalItemId.Value);
|
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<CalendarItem> 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)
|
private AccountCalendarViewModel ResolveAssignedCalendar(Guid calendarId)
|
||||||
=> AccountCalendarStateService.AllCalendars.FirstOrDefault(calendar => calendar.Id == calendarId);
|
=> AccountCalendarStateService.AllCalendars.FirstOrDefault(calendar => calendar.Id == calendarId);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum CalendarContextMenuActionType
|
||||||
|
{
|
||||||
|
Open,
|
||||||
|
JoinOnline,
|
||||||
|
Delete,
|
||||||
|
ShowAs,
|
||||||
|
Respond
|
||||||
|
}
|
||||||
@@ -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<CalendarContextMenuItem> GetContextMenuItems(CalendarItem calendarItem);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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<CalendarContextMenuItem> ChildItems = null)
|
||||||
|
{
|
||||||
|
public IReadOnlyList<CalendarContextMenuItem> Children { get; init; } = ChildItems ?? [];
|
||||||
|
|
||||||
|
public bool HasChildren => Children.Count > 0;
|
||||||
|
}
|
||||||
@@ -192,6 +192,7 @@
|
|||||||
"CalendarEventDetails_Organizer": "Organizer",
|
"CalendarEventDetails_Organizer": "Organizer",
|
||||||
"CalendarEventDetails_People": "People",
|
"CalendarEventDetails_People": "People",
|
||||||
"CalendarEventDetails_ReadOnlyEvent": "Read-only event",
|
"CalendarEventDetails_ReadOnlyEvent": "Read-only event",
|
||||||
|
"CalendarContextMenu_Respond": "Respond",
|
||||||
"CalendarEventDetails_Reminder": "Reminder",
|
"CalendarEventDetails_Reminder": "Reminder",
|
||||||
"CalendarReminder_StartedHoursAgo": "Started {0} hours ago",
|
"CalendarReminder_StartedHoursAgo": "Started {0} hours ago",
|
||||||
"CalendarReminder_StartedMinutesAgo": "Started {0} minutes ago",
|
"CalendarReminder_StartedMinutesAgo": "Started {0} minutes ago",
|
||||||
@@ -207,7 +208,9 @@
|
|||||||
"CalendarShowAs_Busy": "Busy",
|
"CalendarShowAs_Busy": "Busy",
|
||||||
"CalendarShowAs_OutOfOffice": "Out of Office",
|
"CalendarShowAs_OutOfOffice": "Out of Office",
|
||||||
"CalendarShowAs_WorkingElsewhere": "Working Elsewhere",
|
"CalendarShowAs_WorkingElsewhere": "Working Elsewhere",
|
||||||
|
"CalendarContextMenu_AllEventsInSeries": "All events in the series",
|
||||||
"CalendarItem_DetailsPopup_JoinOnline": "Join online",
|
"CalendarItem_DetailsPopup_JoinOnline": "Join online",
|
||||||
|
"CalendarContextMenu_ThisEventOnly": "This event only",
|
||||||
"CalendarItem_DetailsPopup_ViewEventButton": "View event",
|
"CalendarItem_DetailsPopup_ViewEventButton": "View event",
|
||||||
"CalendarItem_DetailsPopup_ViewSeriesButton": "View series",
|
"CalendarItem_DetailsPopup_ViewSeriesButton": "View series",
|
||||||
"CalendarDragDropMoveNotAllowedMessage": "Only events you own and can edit can be moved.",
|
"CalendarDragDropMoveNotAllowedMessage": "Only events you own and can edit can be moved.",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Wino.Core.Domain.Models.Calendar;
|
|||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Messaging.Client.Calendar;
|
using Wino.Messaging.Client.Calendar;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Wino.Calendar.ViewModels.Messages;
|
||||||
|
|
||||||
namespace Wino.Core.Tests;
|
namespace Wino.Core.Tests;
|
||||||
|
|
||||||
@@ -311,6 +312,22 @@ public class CalendarPageViewModelTests
|
|||||||
accountCalendarViewModel.MailAccount.Should().BeSameAs(account);
|
accountCalendarViewModel.MailAccount.Should().BeSameAs(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReceiveCalendarItemRightTappedMessage_SelectsItemForDetails()
|
||||||
|
{
|
||||||
|
var settings = CreateSettings();
|
||||||
|
var preferencesService = CreatePreferencesService(settings);
|
||||||
|
var calendarService = new Mock<ICalendarService>();
|
||||||
|
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(
|
private static CalendarPageViewModel CreateViewModel(
|
||||||
ICalendarService calendarService,
|
ICalendarService calendarService,
|
||||||
IPreferencesService preferencesService,
|
IPreferencesService preferencesService,
|
||||||
@@ -330,6 +347,17 @@ public class CalendarPageViewModelTests
|
|||||||
IPreferencesService preferencesService,
|
IPreferencesService preferencesService,
|
||||||
DateOnly today,
|
DateOnly today,
|
||||||
IAccountCalendarStateService accountCalendarStateService)
|
IAccountCalendarStateService accountCalendarStateService)
|
||||||
|
=> CreateViewModel(calendarService, preferencesService, today, accountCalendarStateService, navigationService: Mock.Of<INavigationService>());
|
||||||
|
|
||||||
|
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<IStatePersistanceService>();
|
var statePersistenceService = new Mock<IStatePersistanceService>();
|
||||||
statePersistenceService.SetupAllProperties();
|
statePersistenceService.SetupAllProperties();
|
||||||
@@ -339,13 +367,13 @@ public class CalendarPageViewModelTests
|
|||||||
return new CalendarPageViewModel(
|
return new CalendarPageViewModel(
|
||||||
statePersistenceService.Object,
|
statePersistenceService.Object,
|
||||||
calendarService,
|
calendarService,
|
||||||
Mock.Of<INavigationService>(),
|
navigationService ?? Mock.Of<INavigationService>(),
|
||||||
Mock.Of<IKeyPressService>(),
|
Mock.Of<IKeyPressService>(),
|
||||||
Mock.Of<INativeAppService>(),
|
nativeAppService ?? Mock.Of<INativeAppService>(),
|
||||||
accountCalendarStateService,
|
accountCalendarStateService,
|
||||||
preferencesService,
|
preferencesService,
|
||||||
Mock.Of<IWinoRequestDelegator>(),
|
requestDelegator ?? Mock.Of<IWinoRequestDelegator>(),
|
||||||
Mock.Of<IMailDialogService>(),
|
dialogService ?? Mock.Of<IMailDialogService>(),
|
||||||
new TestDateContextProvider("en-US", today),
|
new TestDateContextProvider("en-US", today),
|
||||||
new CalendarRangeTextFormatter());
|
new CalendarRangeTextFormatter());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Calendar.ViewModels.Messages;
|
||||||
using Wino.Calendar.ViewModels.Data;
|
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;
|
namespace Wino.Calendar.Controls;
|
||||||
|
|
||||||
public partial class CalendarItemCommandBarFlyout : CommandBarFlyout
|
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 static readonly DependencyProperty ItemProperty = DependencyProperty.Register(nameof(Item), typeof(CalendarItemViewModel), typeof(CalendarItemCommandBarFlyout), new PropertyMetadata(null, new PropertyChangedCallback(OnItemChanged)));
|
||||||
|
|
||||||
public CalendarItemViewModel Item
|
public CalendarItemViewModel Item
|
||||||
@@ -14,6 +26,11 @@ public partial class CalendarItemCommandBarFlyout : CommandBarFlyout
|
|||||||
set { SetValue(ItemProperty, value); }
|
set { SetValue(ItemProperty, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CalendarItemCommandBarFlyout()
|
||||||
|
{
|
||||||
|
_contextMenuItemService = WinoApplication.Current.Services.GetRequiredService<ICalendarContextMenuItemService>();
|
||||||
|
Opening += FlyoutOpening;
|
||||||
|
}
|
||||||
|
|
||||||
private static void OnItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
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()
|
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<MenuFlyoutItemBase> items, IReadOnlyList<CalendarContextMenuItem> 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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,9 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Grid.ContextFlyout>
|
<Grid.ContextFlyout>
|
||||||
<local:CalendarItemCommandBarFlyout Placement="Top">
|
<local:CalendarItemCommandBarFlyout
|
||||||
<local:CalendarItemCommandBarFlyout.PrimaryCommands>
|
Item="{x:Bind CalendarItem, Mode=OneWay}"
|
||||||
<AppBarButton Icon="Save" Label="save" />
|
Placement="Top" />
|
||||||
<AppBarButton Icon="Delete" Label="Delet" />
|
|
||||||
</local:CalendarItemCommandBarFlyout.PrimaryCommands>
|
|
||||||
</local:CalendarItemCommandBarFlyout>
|
|
||||||
</Grid.ContextFlyout>
|
</Grid.ContextFlyout>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,14 @@ public sealed partial class CalendarItemControl : UserControl
|
|||||||
|
|
||||||
private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
|
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));
|
WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<CalendarItemShowAs> ShowAsOptions =
|
||||||
|
[
|
||||||
|
CalendarItemShowAs.Free,
|
||||||
|
CalendarItemShowAs.Tentative,
|
||||||
|
CalendarItemShowAs.Busy,
|
||||||
|
CalendarItemShowAs.OutOfOffice,
|
||||||
|
CalendarItemShowAs.WorkingElsewhere
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<CalendarItemStatus> ResponseOptions =
|
||||||
|
[
|
||||||
|
CalendarItemStatus.Accepted,
|
||||||
|
CalendarItemStatus.Tentative,
|
||||||
|
CalendarItemStatus.Cancelled
|
||||||
|
];
|
||||||
|
|
||||||
|
public IReadOnlyList<CalendarContextMenuItem> GetContextMenuItems(CalendarItem calendarItem)
|
||||||
|
{
|
||||||
|
if (calendarItem == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var items = new List<CalendarContextMenuItem>
|
||||||
|
{
|
||||||
|
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<CalendarContextMenuItem> CreateShowAsLeaves(CalendarEventTargetType targetType)
|
||||||
|
{
|
||||||
|
var items = new List<CalendarContextMenuItem>(ShowAsOptions.Count);
|
||||||
|
|
||||||
|
foreach (var showAs in ShowAsOptions)
|
||||||
|
{
|
||||||
|
items.Add(new CalendarContextMenuItem(
|
||||||
|
new CalendarContextMenuAction(CalendarContextMenuActionType.ShowAs, targetType, showAs)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<CalendarContextMenuItem> CreateResponseLeaves(CalendarEventTargetType targetType)
|
||||||
|
{
|
||||||
|
var items = new List<CalendarContextMenuItem>(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));
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ public static class ServicesContainerSetup
|
|||||||
services.AddTransient<ISignatureService, SignatureService>();
|
services.AddTransient<ISignatureService, SignatureService>();
|
||||||
services.AddTransient<IEmailTemplateService, EmailTemplateService>();
|
services.AddTransient<IEmailTemplateService, EmailTemplateService>();
|
||||||
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
|
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
|
||||||
|
services.AddTransient<ICalendarContextMenuItemService, CalendarContextMenuItemService>();
|
||||||
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
|
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
|
||||||
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
|
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
|
||||||
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
|
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
|
||||||
|
|||||||
Reference in New Issue
Block a user