RSVP options.

This commit is contained in:
Burak Kaan Köse
2026-01-03 19:33:36 +01:00
parent a64627e7d6
commit 9877656eea
28 changed files with 968 additions and 115 deletions
@@ -66,4 +66,5 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
}
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
}
@@ -36,7 +36,10 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
[NotifyPropertyChangedFor(nameof(CanEditSeries))]
[NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))]
[NotifyPropertyChangedFor(nameof(CurrentRsvpText))]
[NotifyPropertyChangedFor(nameof(CurrentRsvpStatus))]
public partial CalendarItemViewModel CurrentEvent { get; set; }
[ObservableProperty]
public partial CalendarItemViewModel SeriesParent { get; set; }
[ObservableProperty]
@@ -80,6 +83,45 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
#endregion
#region RSVP Panel
[ObservableProperty]
public partial bool IsRsvpPanelVisible { get; set; }
public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
[ObservableProperty]
public partial string RsvpMessage { get; set; } = string.Empty;
public ObservableCollection<RsvpStatusOption> RsvpStatusOptions { get; } = new ObservableCollection<RsvpStatusOption>();
public CalendarItemStatus CurrentRsvpStatus
{
get
{
return CurrentEvent?.CalendarItem?.Status ?? CalendarItemStatus.NotResponded;
}
}
public string CurrentRsvpText
{
get
{
if (CurrentEvent?.CalendarItem == null) return Translator.CalendarEventResponse_Accept;
return CurrentEvent.CalendarItem.Status switch
{
CalendarItemStatus.Accepted => Translator.CalendarEventResponse_AcceptedResponse,
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
_ => throw new NotImplementedException()
};
}
}
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService,
INativeAppService nativeAppService,
IPreferencesService preferencesService,
@@ -95,6 +137,11 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
_navigationService = navigationService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
// Initialize RSVP status options
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Accepted));
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Tentative));
RsvpStatusOptions.Add(new RsvpStatusOption(CalendarItemStatus.Cancelled));
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -109,6 +156,17 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
await LoadCalendarItemTargetAsync(args);
}
protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
{
base.OnCalendarItemDeleted(calendarItem);
// If the current event was deleted, navigate back
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
{
_navigationService.GoBack();
}
}
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
{
try
@@ -265,11 +323,68 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
}
[RelayCommand]
private async Task RespondAsync(CalendarItemStatus status)
private void ToggleRsvpPanel()
{
IsRsvpPanelVisible = !IsRsvpPanelVisible;
if (IsRsvpPanelVisible && CurrentEvent?.CalendarItem != null)
{
// Initialize selection based on current status
foreach (var item in RsvpStatusOptions)
{
item.IsSelected = CurrentEvent?.CalendarItem?.Status == item.Status;
}
}
}
[RelayCommand]
private void CloseRsvpPanel()
{
IsRsvpPanelVisible = false;
RsvpMessage = string.Empty;
}
[RelayCommand]
private async Task SendRsvpResponse(AttendeeStatus status)
{
if (CurrentEvent == null) return;
// TODO: Implement response
try
{
// Get the optional response message if user wants to include it
var responseMessage = IncludeRsvpMessage ? RsvpMessage : null;
// Map status to operation
CalendarSynchronizerOperation operation = status switch
{
AttendeeStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
AttendeeStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent,
AttendeeStatus.Declined => CalendarSynchronizerOperation.DeclineEvent,
_ => throw new InvalidOperationException($"Invalid RSVP status: {status}")
};
// Create preparation request with the optional message
var preparationRequest = new CalendarOperationPreparationRequest(
operation,
CurrentEvent.CalendarItem,
null,
responseMessage);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
OnPropertyChanged(nameof(CurrentRsvpText));
OnPropertyChanged(nameof(CurrentRsvpStatus));
CloseRsvpPanel();
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending RSVP response: {ex.Message}");
_dialogService.InfoBarMessage(
Translator.Info_AttachmentSaveFailedTitle,
ex.Message,
InfoBarMessageType.Error);
}
}
[RelayCommand]
@@ -323,3 +438,30 @@ public partial class ReminderOption : ObservableObject
IsCustom = isCustom;
}
}
public partial class RsvpStatusOption : ObservableObject
{
public CalendarItemStatus Status { get; }
public string StatusText
{
get
{
return Status switch
{
CalendarItemStatus.Accepted => Translator.CalendarEventResponse_Accept,
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_Tentative,
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_Decline,
_ => Translator.CalendarEventResponse_Accept
};
}
}
[ObservableProperty]
public partial bool IsSelected { get; set; }
public RsvpStatusOption(CalendarItemStatus status)
{
Status = status;
}
}
+16 -2
View File
@@ -258,18 +258,32 @@
Height="40"
DisplayName="{x:Bind Name}" />
<!-- TODO: Organizer -->
<Grid Grid.Column="1">
<Grid Grid.Column="1" RowSpacing="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
<TextBlock
Grid.Row="1"
FontSize="13"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Email}" />
<Border
Grid.Row="2"
Padding="6,2"
HorizontalAlignment="Left"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{x:Bind IsOrganizer}">
<TextBlock
FontSize="11"
FontWeight="SemiBold"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Text="{x:Bind domain:Translator.CalendarEventDetails_Organizer}" />
</Border>
</Grid>
</Grid>
</DataTemplate>
@@ -1,5 +1,6 @@
using System;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
@@ -22,4 +23,7 @@ public class AccountCalendar : IAccountCalendar
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; }
[Ignore]
public MailAccount MailAccount { get; set; }
}
+1 -1
View File
@@ -3,7 +3,7 @@
public enum CalendarItemStatus
{
NotResponded,
Confirmed,
Accepted,
Tentative,
Cancelled,
}
+3
View File
@@ -26,6 +26,9 @@ public enum CalendarSynchronizerOperation
CreateEvent,
UpdateEvent,
DeleteEvent,
AcceptEvent,
DeclineEvent,
TentativeEvent,
}
// UI requests
@@ -1,4 +1,5 @@
using System;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Interfaces;
@@ -12,4 +13,5 @@ public interface IAccountCalendar
string RemoteCalendarId { get; set; }
bool IsExtended { get; set; }
Guid Id { get; set; }
MailAccount MailAccount { get; set; }
}
@@ -7,7 +7,8 @@ namespace Wino.Core.Domain.Models.Calendar;
/// <summary>
/// Encapsulates the options for preparing calendar operation requests.
/// </summary>
/// <param name="Operation">Calendar operation to execute (Create, Update, Delete).</param>
/// <param name="Operation">Calendar operation to execute (Create, Update, Delete, Accept, Decline, Tentative).</param>
/// <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);
/// <param name="ResponseMessage">Optional message to include with event responses (Accept, Decline, Tentative).</param>
public record CalendarOperationPreparationRequest(CalendarSynchronizerOperation Operation, CalendarItem CalendarItem, List<CalendarEventAttendee> Attendees, string ResponseMessage = null);
@@ -84,14 +84,26 @@
"CalendarDisplayOptions_Color": "Color",
"CalendarDisplayOptions_Expand": "Expand",
"CalendarEventResponse_Accept": "Accept",
"CalendarEventResponse_AcceptedResponse": "Accepted",
"CalendarEventResponse_Decline": "Decline",
"CalendarEventResponse_DeclinedResponse": "Declined",
"CalendarEventResponse_NotResponded": "Not Responded",
"CalendarEventResponse_Tentative": "Tentative",
"CalendarEventResponse_TentativeResponse": "Tentative",
"CalendarEventRsvpPanel_Accept": "Accept",
"CalendarEventRsvpPanel_AddMessage": "Add a message to your response... (optional)",
"CalendarEventRsvpPanel_Decline": "Decline",
"CalendarEventRsvpPanel_Message": "Message",
"CalendarEventRsvpPanel_SendReplyMessage": "Send a reply message",
"CalendarEventRsvpPanel_Tentative": "Tentative",
"CalendarEventRsvpPanel_Title": "Response Options",
"CalendarEventDetails_Attachments": "Attachments",
"CalendarEventDetails_Details": "Details",
"CalendarEventDetails_EditSeries": "Edit Series",
"CalendarEventDetails_Editing": "Editing",
"CalendarEventDetails_InviteSomeone": "Invite someone",
"CalendarEventDetails_JoinOnline": "Join Online",
"CalendarEventDetails_Organizer": "Organizer",
"CalendarEventDetails_People": "People",
"CalendarEventDetails_ReadOnlyEvent": "Read-only event",
"CalendarEventDetails_Reminder": "Reminder",
@@ -349,10 +349,10 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
{
return status switch
{
"confirmed" => CalendarItemStatus.Confirmed,
"confirmed" => CalendarItemStatus.Accepted,
"tentative" => CalendarItemStatus.Tentative,
"cancelled" => CalendarItemStatus.Cancelled,
_ => CalendarItemStatus.Confirmed
_ => CalendarItemStatus.Accepted
};
}
@@ -152,7 +152,7 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
break;
case ResponseType.Accepted:
case ResponseType.Organizer:
savingItem.Status = CalendarItemStatus.Confirmed;
savingItem.Status = CalendarItemStatus.Accepted;
break;
case ResponseType.Declined:
savingItem.Status = CalendarItemStatus.Cancelled;
@@ -164,7 +164,7 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
}
else
{
savingItem.Status = CalendarItemStatus.Confirmed;
savingItem.Status = CalendarItemStatus.Accepted;
}
// Prepare attendees list
@@ -0,0 +1,39 @@
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 accept a calendar event invitation on the server.
/// The calendar item status should be updated locally before queuing this request.
/// </summary>
public record AcceptEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item)
{
private readonly CalendarItemStatus _previousStatus = Item.Status;
public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.AcceptEvent;
/// <summary>
/// After successful acceptance, we need to resync to get updated status.
/// </summary>
public override int ResynchronizationDelay => 2000;
public override void ApplyUIChanges()
{
// Update the item status locally
Item.Status = CalendarItemStatus.Accepted;
// Notify UI that the event status was updated
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
}
public override void RevertUIChanges()
{
// If acceptance fails, revert to the previous status
Item.Status = _previousStatus;
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
}
}
@@ -0,0 +1,39 @@
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 decline a calendar event invitation on the server.
/// The calendar item status should be updated locally before queuing this request.
/// </summary>
public record DeclineEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item)
{
private readonly CalendarItemStatus _previousStatus = Item.Status;
public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeclineEvent;
/// <summary>
/// After successful decline, we need to resync to get updated status.
/// </summary>
public override int ResynchronizationDelay => 2000;
public override void ApplyUIChanges()
{
// Update the item status locally
Item.Status = CalendarItemStatus.Cancelled;
// Notify UI that the event status was updated
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
}
public override void RevertUIChanges()
{
// If decline fails, revert to the previous status
Item.Status = _previousStatus;
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
}
}
@@ -0,0 +1,38 @@
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>
/// Outlook-specific request to decline a calendar event invitation.
/// In Outlook, declined events are removed from the calendar by the API after synchronization,
/// so this request sends a delete notification to remove the event from the UI.
/// </summary>
public record OutlookDeclineEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item)
{
private readonly CalendarItemStatus _previousStatus = Item.Status;
public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.DeclineEvent;
/// <summary>
/// After successful decline, we need to resync to confirm the event is removed.
/// </summary>
public override int ResynchronizationDelay => 2000;
public override void ApplyUIChanges()
{
// In Outlook, declined events are deleted from the calendar after sync
// Send deleted message to remove from UI immediately
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(Item));
}
public override void RevertUIChanges()
{
// If decline fails, restore the previous status and re-add the event
Item.Status = _previousStatus;
WeakReferenceMessenger.Default.Send(new CalendarItemAdded(Item));
}
}
@@ -0,0 +1,39 @@
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 tentatively accept a calendar event invitation on the server.
/// The calendar item status should be updated locally before queuing this request.
/// </summary>
public record TentativeEventRequest(CalendarItem Item, string ResponseMessage = null) : CalendarRequestBase(Item)
{
private readonly CalendarItemStatus _previousStatus = Item.Status;
public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.TentativeEvent;
/// <summary>
/// After successful tentative acceptance, we need to resync to get updated status.
/// </summary>
public override int ResynchronizationDelay => 2000;
public override void ApplyUIChanges()
{
// Update the item status locally
Item.Status = CalendarItemStatus.Tentative;
// Notify UI that the event status was updated
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
}
public override void RevertUIChanges()
{
// If tentative acceptance fails, revert to the previous status
Item.Status = _previousStatus;
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
}
}
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
@@ -141,6 +142,9 @@ public class WinoRequestDelegator : IWinoRequestDelegator
{
CalendarSynchronizerOperation.CreateEvent => new CreateCalendarEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.Attendees),
CalendarSynchronizerOperation.DeleteEvent => new DeleteCalendarEventRequest(calendarPreparationRequest.CalendarItem),
CalendarSynchronizerOperation.AcceptEvent => new AcceptEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.ResponseMessage),
CalendarSynchronizerOperation.DeclineEvent => CreateDeclineRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.ResponseMessage),
CalendarSynchronizerOperation.TentativeEvent => new TentativeEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.ResponseMessage),
// Future support for update operations
// CalendarSynchronizerOperation.UpdateEvent => new UpdateCalendarEventRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.Attendees),
_ => throw new NotImplementedException($"Calendar operation {calendarPreparationRequest.Operation} is not implemented yet.")
@@ -150,6 +154,18 @@ public class WinoRequestDelegator : IWinoRequestDelegator
await QueueCalendarSynchronizationAsync(calendarPreparationRequest.CalendarItem.AssignedCalendar.AccountId);
}
private IRequestBase CreateDeclineRequest(CalendarItem calendarItem, string responseMessage)
{
// For Outlook accounts, declined events are deleted by the server after synchronization.
// Use OutlookDeclineEventRequest to handle UI removal.
if (calendarItem.AssignedCalendar?.MailAccount?.ProviderType == MailProviderType.Outlook)
{
return new OutlookDeclineEventRequest(calendarItem, responseMessage);
}
return new DeclineEventRequest(calendarItem, responseMessage);
}
private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
// Don't trigger synchronization for individual requests - we'll trigger it once for all requests
+121 -1
View File
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using System.Web;
using CommunityToolkit.Mvvm.Messaging;
using Google;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data;
@@ -1648,7 +1649,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
Summary = calendarItem.Title,
Description = calendarItem.Description,
Location = calendarItem.Location,
Status = calendarItem.Status == CalendarItemStatus.Confirmed ? "confirmed" : "tentative"
Status = calendarItem.Status == CalendarItemStatus.Accepted ? "confirmed" : "tentative"
};
// Set start and end time
@@ -1696,6 +1697,125 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
return [new HttpRequestBundle<IClientServiceRequest>(insertRequest, request)];
}
public override List<IRequestBundle<IClientServiceRequest>> AcceptEvent(AcceptEventRequest request)
{
var calendarItem = request.Item;
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
throw new InvalidOperationException("Cannot accept event without remote event ID");
}
// For Gmail, we need to patch the event with the user's response status
// Get the current user's email from the account
var userEmail = Account.Address;
// Create a patch event to update only the attendee response
var patchEvent = new Event();
// We need to get the event first to update the specific attendee
// However, for efficiency, we'll use the patch method with sendUpdates parameter
var patchRequest = _calendarService.Events.Patch(new Event
{
// The API will handle updating the current user's attendee status
Attendees = new List<EventAttendee>
{
new EventAttendee
{
Email = userEmail,
ResponseStatus = "accepted"
}
}
}, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
// Send updates to other attendees if there's a message
patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage)
? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All
: Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None;
return [new HttpRequestBundle<IClientServiceRequest>(patchRequest, request)];
}
public override List<IRequestBundle<IClientServiceRequest>> DeclineEvent(DeclineEventRequest request)
{
var calendarItem = request.Item;
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
throw new InvalidOperationException("Cannot decline event without remote event ID");
}
var userEmail = Account.Address;
var patchRequest = _calendarService.Events.Patch(new Event
{
Attendees = new List<EventAttendee>
{
new EventAttendee
{
Email = userEmail,
ResponseStatus = "declined",
Comment = request.ResponseMessage
}
}
}, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage)
? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All
: Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None;
return [new HttpRequestBundle<IClientServiceRequest>(patchRequest, request)];
}
public override List<IRequestBundle<IClientServiceRequest>> TentativeEvent(TentativeEventRequest request)
{
var calendarItem = request.Item;
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
throw new InvalidOperationException("Cannot tentatively accept event without remote event ID");
}
var userEmail = Account.Address;
var patchRequest = _calendarService.Events.Patch(new Event
{
Attendees = new List<EventAttendee>
{
new EventAttendee
{
Email = userEmail,
ResponseStatus = "tentative",
Comment = request.ResponseMessage
}
}
}, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage)
? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All
: Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.None;
return [new HttpRequestBundle<IClientServiceRequest>(patchRequest, request)];
}
#endregion
public override async Task KillSynchronizerAsync()
@@ -1680,6 +1680,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
foreach (var item in events)
{
// Declined events are returned as Deleted from the API.
// There is no way to distinguish unfortunately atm.
if (IsResourceDeleted(item.AdditionalData))
{
await _outlookChangeProcessor.DeleteCalendarItemAsync(item.Id, calendar.Id).ConfigureAwait(false);
@@ -1886,6 +1889,80 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
return [new HttpRequestBundle<RequestInformation>(createRequest, request)];
}
public override List<IRequestBundle<RequestInformation>> AcceptEvent(AcceptEventRequest request)
{
var calendarItem = request.Item;
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
throw new InvalidOperationException("Cannot accept event without remote event ID");
}
var acceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].Accept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Accept.AcceptPostRequestBody
{
Comment = request.ResponseMessage,
SendResponse = !string.IsNullOrEmpty(request.ResponseMessage)
});
return [new HttpRequestBundle<RequestInformation>(acceptRequestInfo, request)];
}
public override List<IRequestBundle<RequestInformation>> OutlookDeclineEvent(OutlookDeclineEventRequest request)
{
var responseMessage = request.ResponseMessage;
var calendarItem = request.Item;
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
throw new InvalidOperationException("Cannot decline event without remote event ID");
}
var declineRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].Decline.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Decline.DeclinePostRequestBody
{
Comment = responseMessage,
SendResponse = !string.IsNullOrEmpty(responseMessage)
});
return [new HttpRequestBundle<RequestInformation>(declineRequestInfo, request)];
}
public override List<IRequestBundle<RequestInformation>> TentativeEvent(TentativeEventRequest request)
{
var calendarItem = request.Item;
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
throw new InvalidOperationException("Cannot tentatively accept event without remote event ID");
}
var tentativelyAcceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].TentativelyAccept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.TentativelyAccept.TentativelyAcceptPostRequestBody
{
Comment = request.ResponseMessage,
SendResponse = !string.IsNullOrEmpty(request.ResponseMessage)
});
return [new HttpRequestBundle<RequestInformation>(tentativelyAcceptRequestInfo, request)];
}
#endregion
public override async Task KillSynchronizerAsync()
@@ -364,6 +364,22 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
case CalendarSynchronizerOperation.CreateEvent:
nativeRequests.AddRange(CreateCalendarEvent(group.ElementAt(0) as CreateCalendarEventRequest));
break;
case CalendarSynchronizerOperation.AcceptEvent:
nativeRequests.AddRange(AcceptEvent(group.ElementAt(0) as AcceptEventRequest));
break;
case CalendarSynchronizerOperation.DeclineEvent:
if (Account.ProviderType == MailProviderType.Outlook)
{
nativeRequests.AddRange(OutlookDeclineEvent(group.ElementAt(0) as OutlookDeclineEventRequest));
}
else
{
nativeRequests.AddRange(DeclineEvent(group.ElementAt(0) as DeclineEventRequest));
}
break;
case CalendarSynchronizerOperation.TentativeEvent:
nativeRequests.AddRange(TentativeEvent(group.ElementAt(0) as TentativeEventRequest));
break;
case CalendarSynchronizerOperation.UpdateEvent:
// TODO: Implement UpdateCalendarEvent
break;
@@ -494,6 +510,10 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
#region Calendar Operations
public virtual List<IRequestBundle<TBaseRequest>> CreateCalendarEvent(CreateCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> AcceptEvent(AcceptEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> DeclineEvent(DeclineEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> OutlookDeclineEvent(OutlookDeclineEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
public virtual List<IRequestBundle<TBaseRequest>> TentativeEvent(TentativeEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
#endregion
Binary file not shown.
+10 -1
View File
@@ -100,6 +100,15 @@ public static class ControlConstants
{ WinoIconGlyph.EventJoinOnline, "\uE926" },
{ WinoIconGlyph.ViewMessageSource, "\uE943" },
{ WinoIconGlyph.Apple, "\uE92B" },
{ WinoIconGlyph.Yahoo, "\uE92C" }
{ WinoIconGlyph.Yahoo, "\uE92C" },
{ WinoIconGlyph.People, "\uF007" },
{ WinoIconGlyph.AttachmentNew, "\uF006" },
{ WinoIconGlyph.CalendarSettings, "\uF005" },
{ WinoIconGlyph.SettingsNew, "\uF004" },
{ WinoIconGlyph.ManageAccounts, "\uF003" },
{ WinoIconGlyph.SendNew, "\uF002" },
{ WinoIconGlyph.CalendarShowAs, "\uF001" },
{ WinoIconGlyph.EventDecline, "\uF000" },
{ WinoIconGlyph.Dismiss, "\uF008" },
};
}
+9 -1
View File
@@ -101,7 +101,15 @@ public enum WinoIconGlyph
EventJoinOnline,
ViewMessageSource,
Apple,
Yahoo
Yahoo,
People,
AttachmentNew,
CalendarSettings,
SettingsNew,
ManageAccounts,
SendNew,
CalendarShowAs,
Dismiss
}
public partial class WinoFontIcon : FontIcon
@@ -0,0 +1,30 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
namespace Wino.Mail.WinUI.Selectors;
public partial class RsvpStatusIconTemplateSelector : DataTemplateSelector
{
public DataTemplate NotRespondedTemplate { get; set; }
public DataTemplate ConfirmedTemplate { get; set; }
public DataTemplate TentativeTemplate { get; set; }
public DataTemplate CancelledTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item is CalendarItemStatus status)
{
return status switch
{
CalendarItemStatus.NotResponded => NotRespondedTemplate,
CalendarItemStatus.Accepted => ConfirmedTemplate,
CalendarItemStatus.Tentative => TentativeTemplate,
CalendarItemStatus.Cancelled => CancelledTemplate,
_ => NotRespondedTemplate
};
}
return base.SelectTemplateCore(item, container);
}
}
File diff suppressed because one or more lines are too long
@@ -8,16 +8,64 @@
xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
xmlns:ctControls="using:CommunityToolkit.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"
xmlns:selectors="using:Wino.Mail.WinUI.Selectors"
Style="{StaticResource PageStyle}"
mc:Ignorable="d">
<Page.Resources>
<selectors:RsvpStatusIconTemplateSelector x:Key="RsvpStatusIconSelector">
<selectors:RsvpStatusIconTemplateSelector.NotRespondedTemplate>
<DataTemplate>
<coreControls:WinoFontIcon
FontSize="20"
Foreground="{ThemeResource SystemAccentColor}"
Icon="EventRespond" />
</DataTemplate>
</selectors:RsvpStatusIconTemplateSelector.NotRespondedTemplate>
<selectors:RsvpStatusIconTemplateSelector.ConfirmedTemplate>
<DataTemplate>
<coreControls:WinoFontIcon
FontSize="20"
Foreground="#527257"
Icon="EventAccept" />
</DataTemplate>
</selectors:RsvpStatusIconTemplateSelector.ConfirmedTemplate>
<selectors:RsvpStatusIconTemplateSelector.TentativeTemplate>
<DataTemplate>
<coreControls:WinoFontIcon
FontSize="20"
Foreground="#805682"
Icon="EventTentative" />
</DataTemplate>
</selectors:RsvpStatusIconTemplateSelector.TentativeTemplate>
<selectors:RsvpStatusIconTemplateSelector.CancelledTemplate>
<DataTemplate>
<coreControls:WinoFontIcon
FontSize="20"
Foreground="{ThemeResource DeleteBrush}"
Icon="EventDecline" />
</DataTemplate>
</selectors:RsvpStatusIconTemplateSelector.CancelledTemplate>
</selectors:RsvpStatusIconTemplateSelector>
<Style
x:Key="TransparentActionButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="12,8" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
<Style x:Key="ActionBarElementContainerStackStyle" TargetType="StackPanel">
<Setter Property="Spacing" Value="6" />
<Setter Property="Padding" Value="10,0,4,0" />
@@ -25,10 +73,6 @@
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style TargetType="AppBarElementContainer">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="EventDetailsPanelGridStyle" TargetType="Grid">
<Setter Property="Padding" Value="12,6" />
<Setter Property="Margin" Value="0,12" />
@@ -46,6 +90,7 @@
</Page.Resources>
<Grid Padding="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
@@ -57,92 +102,100 @@
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="7">
<CommandBar
HorizontalAlignment="Left"
Background="Transparent"
DefaultLabelPosition="Right"
IsSticky="True"
OverflowButtonVisibility="Auto">
<AppBarToggleButton
x:Name="ReadOnlyToggle"
Content="{x:Bind domain:Translator.CalendarEventDetails_ReadOnlyEvent}"
IsChecked="True" />
<AppBarButton Command="{x:Bind ViewModel.SaveCommand}" Label="{x:Bind domain:Translator.Buttons_Save}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Save" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Command="{x:Bind ViewModel.DeleteCommand}" Label="{x:Bind domain:Translator.Buttons_Delete}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Delete" />
</AppBarButton.Icon>
</AppBarButton>
<Grid Padding="8">
<ctControls:WrapPanel
x:Name="ActionBarWrapGrid"
HorizontalAlignment="Left"
HorizontalSpacing="4"
VerticalSpacing="4">
<AppBarSeparator />
<!-- Read Only Toggle -->
<ToggleButton
x:Name="ReadOnlyToggle"
IsChecked="True"
Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Blocked" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_ReadOnlyEvent}" />
</StackPanel>
</ToggleButton>
<!-- 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>
</AppBarButton>
<!-- Save -->
<Button Command="{x:Bind ViewModel.SaveCommand}" Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Save" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Save}" />
</StackPanel>
</Button>
<AppBarSeparator />
<!-- Delete -->
<Button Command="{x:Bind ViewModel.DeleteCommand}" Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Delete" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Delete}" />
</StackPanel>
</Button>
<!-- Response Options -->
<Border
Width="1"
Height="24"
Margin="4,0"
VerticalAlignment="Center"
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
<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>
<!-- Join Online -->
<Button
Command="{x:Bind ViewModel.JoinOnlineCommand}"
IsEnabled="{x:Bind calendarHelpers:CalendarXamlHelpers.HasOnlineMeetingLink(ViewModel.CurrentEvent), Mode=OneWay}"
Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="EventJoinOnline" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_JoinOnline}" />
</StackPanel>
</Button>
<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>
<Border
Width="1"
Height="24"
Margin="4,0"
VerticalAlignment="Center"
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
<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>
</AppBarButton>
<!-- RSVP Button -->
<Button Command="{x:Bind ViewModel.ToggleRsvpPanelCommand}" Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<ContentControl Content="{x:Bind ViewModel.CurrentRsvpStatus, Mode=OneWay}" ContentTemplateSelector="{StaticResource RsvpStatusIconSelector}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.CurrentRsvpText, Mode=OneWay}" />
</StackPanel>
</Button>
<AppBarSeparator />
<Border
Width="1"
Height="24"
Margin="4,0"
VerticalAlignment="Center"
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
<!-- Show as -->
<AppBarElementContainer>
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_ShowAs}" />
<!-- Show as -->
<StackPanel
Padding="12,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<coreControls:WinoFontIcon FontSize="20" Icon="CalendarShowAs" />
<ComboBox
Width="150"
VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.ShowAsOptions}"
SelectedItem="{x:Bind ViewModel.SelectedShowAs, Mode=TwoWay}" />
</StackPanel>
</AppBarElementContainer>
<!-- Reminder -->
<AppBarElementContainer>
<Button>
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_Reminder}" />
</StackPanel>
</Button.Content>
<!-- Reminder -->
<Button Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_Reminder}" />
</StackPanel>
<Button.Flyout>
<Flyout>
<ListView
@@ -159,24 +212,124 @@
</Flyout>
</Button.Flyout>
</Button>
</AppBarElementContainer>
<AppBarSeparator Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}" />
<!-- Edit Series -->
<Border
Width="1"
Height="24"
Margin="4,0"
VerticalAlignment="Center"
Background="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}" />
<!-- Edit Series -->
<AppBarButton
Command="{x:Bind ViewModel.ViewSeriesCommand}"
Label="{x:Bind domain:Translator.CalendarEventDetails_EditSeries}"
Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="EventEditSeries" />
</AppBarButton.Icon>
</AppBarButton>
</CommandBar>
<Button
Command="{x:Bind ViewModel.ViewSeriesCommand}"
Style="{StaticResource TransparentActionButtonStyle}"
Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="EventEditSeries" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventDetails_EditSeries}" />
</StackPanel>
</Button>
</ctControls:WrapPanel>
</Grid>
</Border>
<!-- RSVP Panel -->
<Border
Grid.Row="1"
Margin="0,8,0,0"
Padding="16"
VerticalAlignment="Top"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="7"
Visibility="{x:Bind ViewModel.IsRsvpPanelVisible, Mode=OneWay}">
<Grid RowSpacing="6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Close Button -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Command="{x:Bind ViewModel.CloseRsvpPanelCommand}"
Style="{StaticResource TransparentActionButtonStyle}">
<coreControls:WinoFontIcon FontSize="16" Icon="Dismiss" />
</Button>
<!-- RSVP Buttons -->
<StackPanel Orientation="Horizontal" Spacing="4">
<!-- Accept -->
<Button
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
CommandParameter="{x:Bind enums:AttendeeStatus.Accepted}"
Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon
FontSize="20"
Foreground="#527257"
Icon="EventAccept" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventRsvpPanel_Accept, Mode=OneTime}" />
</StackPanel>
</Button>
<!-- Tentative -->
<Button
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
CommandParameter="{x:Bind enums:AttendeeStatus.Tentative}"
Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon
FontSize="20"
Foreground="#805682"
Icon="EventTentative" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventRsvpPanel_Tentative, Mode=OneTime}" />
</StackPanel>
</Button>
<!-- Decline -->
<Button
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
CommandParameter="{x:Bind enums:AttendeeStatus.Declined}"
Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon
FontSize="20"
Foreground="{ThemeResource DeleteBrush}"
Icon="EventDecline" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventRsvpPanel_Decline, Mode=OneTime}" />
</StackPanel>
</Button>
</StackPanel>
<!-- Reply Message TextBox -->
<TextBox
Grid.Row="1"
Grid.ColumnSpan="2"
VerticalAlignment="Center"
AcceptsReturn="True"
PlaceholderText="{x:Bind domain:Translator.CalendarEventRsvpPanel_AddMessage, Mode=OneTime}"
Text="{x:Bind ViewModel.RsvpMessage, Mode=TwoWay}"
TextWrapping="Wrap">
<TextBox.Header>
<StackPanel Orientation="Horizontal" />
</TextBox.Header>
</TextBox>
</Grid>
</Border>
<!-- Event details -->
<ScrollViewer Grid.Row="1">
<ScrollViewer Grid.Row="2">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
@@ -293,18 +446,32 @@
Height="40"
DisplayName="{x:Bind Name}" />
<!-- TODO: Organizer -->
<Grid Grid.Column="1">
<Grid Grid.Column="1" RowSpacing="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
<TextBlock
Grid.Row="1"
FontSize="13"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Email}" />
<Border
Grid.Row="2"
Padding="6,2"
HorizontalAlignment="Left"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{x:Bind IsOrganizer}">
<TextBlock
FontSize="11"
FontWeight="SemiBold"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Text="{x:Bind domain:Translator.CalendarEventDetails_Organizer}" />
</Border>
</Grid>
</Grid>
</DataTemplate>
@@ -328,5 +495,50 @@
</Grid>
</Grid>
</ScrollViewer>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="WindowWidthStates">
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="1200" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="MediumState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="800" />
</VisualState.StateTriggers>
<VisualState.Setters>
<!-- Details takes left side (full height), People and Attachments share right side vertically -->
<Setter Target="DetailsGrid.(Grid.Row)" Value="0" />
<Setter Target="DetailsGrid.(Grid.Column)" Value="0" />
<Setter Target="DetailsGrid.(Grid.RowSpan)" Value="2" />
<Setter Target="DetailsGrid.(Grid.ColumnSpan)" Value="1" />
<Setter Target="PeopleGrid.(Grid.Row)" Value="0" />
<Setter Target="PeopleGrid.(Grid.Column)" Value="1" />
<Setter Target="PeopleGrid.(Grid.ColumnSpan)" Value="2" />
<Setter Target="AttachmentsGrid.(Grid.Row)" Value="1" />
<Setter Target="AttachmentsGrid.(Grid.Column)" Value="1" />
<Setter Target="AttachmentsGrid.(Grid.ColumnSpan)" Value="2" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NarrowState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<!-- Stack all panels vertically -->
<Setter Target="DetailsGrid.(Grid.Row)" Value="0" />
<Setter Target="DetailsGrid.(Grid.Column)" Value="0" />
<Setter Target="DetailsGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="PeopleGrid.(Grid.Row)" Value="1" />
<Setter Target="PeopleGrid.(Grid.Column)" Value="0" />
<Setter Target="PeopleGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="AttachmentsGrid.(Grid.Row)" Value="2" />
<Setter Target="AttachmentsGrid.(Grid.Column)" Value="0" />
<Setter Target="AttachmentsGrid.(Grid.ColumnSpan)" Value="3" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</abstract:EventDetailsPageAbstract>
+30 -6
View File
@@ -11,6 +11,7 @@ using Itenso.TimePeriod;
using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
@@ -253,14 +254,33 @@ public class CalendarService : BaseDatabaseService, ICalendarService
return result;
}
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
public Task<CalendarItem> GetCalendarItemAsync(Guid id)
public async Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
{
return Connection.FindWithQueryAsync<CalendarItem>(
var calendar = await Connection.GetAsync<AccountCalendar>(accountCalendarId);
if (calendar != null)
{
calendar.MailAccount = await Connection.GetAsync<MailAccount>(calendar.AccountId);
}
return calendar;
}
public async Task<CalendarItem> GetCalendarItemAsync(Guid id)
{
var calendarItem = await Connection.FindWithQueryAsync<CalendarItem>(
"SELECT * FROM CalendarItem WHERE Id = ?",
id);
// Load assigned calendar and account.
if (calendarItem != null)
{
calendarItem.AssignedCalendar = await Connection.GetAsync<AccountCalendar>(calendarItem.CalendarId);
if (calendarItem.AssignedCalendar != null)
{
calendarItem.AssignedCalendar.MailAccount = await Connection.GetAsync<MailAccount>(calendarItem.AssignedCalendar.AccountId);
}
}
return calendarItem;
}
public async Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId)
@@ -269,10 +289,14 @@ public class CalendarService : BaseDatabaseService, ICalendarService
"SELECT * FROM CalendarItem WHERE CalendarId = ? AND RemoteEventId = ?",
accountCalendarId, remoteEventId);
// Load assigned calendar.
// Load assigned calendar and account.
if (calendarItem != null)
{
calendarItem.AssignedCalendar = await Connection.GetAsync<AccountCalendar>(calendarItem.CalendarId);
if (calendarItem.AssignedCalendar != null)
{
calendarItem.AssignedCalendar.MailAccount = await Connection.GetAsync<MailAccount>(calendarItem.AssignedCalendar.AccountId);
}
}
return calendarItem;
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -10,6 +10,7 @@
<File Path="Directory.Packages.props" />
<File Path="nuget.config" />
<File Path="Settings.XamlStyler" />
<File Path="WinoFontIcomoon.json" />
</Folder>
<Folder Name="/lib/">
<Project Path="Wino.Authentication/Wino.Authentication.csproj">