Event details page improvements, calendar item update source.
This commit is contained in:
@@ -60,29 +60,30 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
{
|
{
|
||||||
base.OnNavigatedTo(mode, parameters);
|
base.OnNavigatedTo(mode, parameters);
|
||||||
|
|
||||||
if (parameters is not Guid accountId)
|
if (parameters is AccountCalendar selectedCalendar)
|
||||||
|
{
|
||||||
|
Account = await _accountService.GetAccountAsync(selectedCalendar.AccountId);
|
||||||
|
AccountCalendar = await _calendarService.GetAccountCalendarAsync(selectedCalendar.Id) ?? selectedCalendar;
|
||||||
|
}
|
||||||
|
else if (parameters is Guid accountId)
|
||||||
|
{
|
||||||
|
Account = await _accountService.GetAccountAsync(accountId);
|
||||||
|
var calendars = await _calendarService.GetAccountCalendarsAsync(accountId);
|
||||||
|
AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Load account
|
if (Account == null || AccountCalendar == null)
|
||||||
Account = await _accountService.GetAccountAsync(accountId);
|
|
||||||
|
|
||||||
if (Account == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Load first primary calendar for this account
|
|
||||||
var calendars = await _calendarService.GetAccountCalendarsAsync(accountId);
|
|
||||||
AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault();
|
|
||||||
|
|
||||||
if (AccountCalendar == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Initialize properties from AccountCalendar
|
// Initialize properties from AccountCalendar
|
||||||
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
||||||
IsSyncEnabled = AccountCalendar.IsExtended;
|
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
|
||||||
IsPrimaryCalendar = AccountCalendar.IsPrimary;
|
IsPrimaryCalendar = AccountCalendar.IsPrimary;
|
||||||
|
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
|
||||||
// TODO: Default ShowAs is not stored in AccountCalendar yet, defaulting to Busy
|
|
||||||
SelectedDefaultShowAsOption = ShowAsOptions[2]; // Busy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnAccountColorHexChanged(string value)
|
partial void OnAccountColorHexChanged(string value)
|
||||||
@@ -98,7 +99,7 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
{
|
{
|
||||||
if (AccountCalendar != null)
|
if (AccountCalendar != null)
|
||||||
{
|
{
|
||||||
AccountCalendar.IsExtended = value;
|
AccountCalendar.IsSynchronizationEnabled = value;
|
||||||
SaveChangesAsync();
|
SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,11 +115,10 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
|||||||
|
|
||||||
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
||||||
{
|
{
|
||||||
// TODO: Default ShowAs should be stored in AccountCalendar or account preferences
|
if (AccountCalendar != null && value != null)
|
||||||
// For now, this is just a placeholder as the property doesn't exist yet
|
|
||||||
if (value != null)
|
|
||||||
{
|
{
|
||||||
// Future: Store value.ShowAs somewhere
|
AccountCalendar.DefaultShowAs = value.ShowAs;
|
||||||
|
SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
Description = string.Empty,
|
Description = string.Empty,
|
||||||
Location = Location ?? string.Empty,
|
Location = Location ?? string.Empty,
|
||||||
Title = EventName,
|
Title = EventName,
|
||||||
|
ShowAs = SelectedQuickEventAccountCalendar.DefaultShowAs,
|
||||||
IsHidden = false,
|
IsHidden = false,
|
||||||
AssignedCalendar = SelectedQuickEventAccountCalendar
|
AssignedCalendar = SelectedQuickEventAccountCalendar
|
||||||
};
|
};
|
||||||
@@ -907,9 +908,9 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem)
|
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
|
||||||
{
|
{
|
||||||
base.OnCalendarItemUpdated(calendarItem);
|
base.OnCalendarItemUpdated(calendarItem, source);
|
||||||
Debug.WriteLine($"Calendar item updated: {calendarItem.Id}");
|
Debug.WriteLine($"Calendar item updated: {calendarItem.Id}");
|
||||||
|
|
||||||
// Series master events should not be visible on the UI.
|
// Series master events should not be visible on the UI.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Wino.Core.Domain.Entities.Calendar;
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
namespace Wino.Calendar.ViewModels.Data;
|
namespace Wino.Calendar.ViewModels.Data;
|
||||||
@@ -54,6 +55,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
|
|||||||
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsSynchronizationEnabled
|
||||||
|
{
|
||||||
|
get => AccountCalendar.IsSynchronizationEnabled;
|
||||||
|
set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i);
|
||||||
|
}
|
||||||
|
|
||||||
public Guid AccountId
|
public Guid AccountId
|
||||||
{
|
{
|
||||||
get => AccountCalendar.AccountId;
|
get => AccountCalendar.AccountId;
|
||||||
@@ -65,6 +72,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
|
|||||||
get => AccountCalendar.RemoteCalendarId;
|
get => AccountCalendar.RemoteCalendarId;
|
||||||
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
|
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CalendarItemShowAs DefaultShowAs
|
||||||
|
{
|
||||||
|
get => AccountCalendar.DefaultShowAs;
|
||||||
|
set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s);
|
||||||
|
}
|
||||||
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
|
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
|
||||||
public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
|
public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
|
public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))]
|
||||||
public partial string RsvpMessage { get; set; } = string.Empty;
|
public partial string RsvpMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
public ObservableCollection<RsvpStatusOption> RsvpStatusOptions { get; } = new ObservableCollection<RsvpStatusOption>();
|
public ObservableCollection<RsvpStatusOption> RsvpStatusOptions { get; } = new ObservableCollection<RsvpStatusOption>();
|
||||||
@@ -129,7 +130,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
|
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
|
||||||
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
|
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
|
||||||
CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
|
CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
|
||||||
_ => throw new NotImplementedException()
|
_ => Translator.CalendarEventResponse_NotResponded
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,19 +180,33 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
await LoadCalendarItemTargetAsync(args);
|
await LoadCalendarItemTargetAsync(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem)
|
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
|
||||||
{
|
{
|
||||||
base.OnCalendarItemUpdated(calendarItem);
|
base.OnCalendarItemUpdated(calendarItem, source);
|
||||||
|
|
||||||
// If the current event was updated, reload it
|
// If the current event was updated, reload it
|
||||||
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
|
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
|
||||||
{
|
{
|
||||||
// Refresh the current event data by reloading from service
|
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
|
||||||
|
if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted)
|
||||||
|
{
|
||||||
|
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
|
||||||
|
CurrentEvent = new CalendarItemViewModel(calendarItem);
|
||||||
|
|
||||||
|
foreach (var attendee in previousAttendees)
|
||||||
|
{
|
||||||
|
CurrentEvent.Attendees.Add(attendee);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh from DB when update comes from server sync.
|
||||||
var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
|
var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
|
||||||
if (refreshedEvent != null)
|
if (refreshedEvent != null)
|
||||||
{
|
{
|
||||||
CurrentEvent = new CalendarItemViewModel(refreshedEvent);
|
CurrentEvent = new CalendarItemViewModel(refreshedEvent);
|
||||||
await LoadAttendeesAsync(refreshedEvent.EventTrackingId, refreshedEvent);
|
await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,17 +233,17 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
|
|
||||||
CurrentEvent = new CalendarItemViewModel(currentEventItem);
|
CurrentEvent = new CalendarItemViewModel(currentEventItem);
|
||||||
|
|
||||||
await LoadAttendeesAsync(currentEventItem.EventTrackingId, currentEventItem);
|
await LoadAttendeesAsync(currentEventItem.Id, currentEventItem);
|
||||||
|
|
||||||
// Initialize SelectedShowAsOption based on current event's ShowAs
|
// Initialize SelectedShowAsOption based on current event's ShowAs
|
||||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2];
|
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2];
|
||||||
|
|
||||||
// Load reminders for this calendar item
|
// Load reminders for this calendar item
|
||||||
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.EventTrackingId);
|
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id);
|
||||||
InitializeReminderOptions();
|
InitializeReminderOptions();
|
||||||
|
|
||||||
// Load attachments
|
// Load attachments
|
||||||
await LoadAttachmentsAsync(currentEventItem.EventTrackingId);
|
await LoadAttachmentsAsync(currentEventItem.Id);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -236,11 +251,11 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAttendeesAsync(Guid eventTrackingId, CalendarItem calendarItem)
|
private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
CurrentEvent.Attendees.Clear();
|
CurrentEvent.Attendees.Clear();
|
||||||
|
|
||||||
var attendees = await _calendarService.GetAttendeesAsync(eventTrackingId);
|
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
|
||||||
|
|
||||||
// Separate organizer from other attendees to ensure organizer is always first
|
// Separate organizer from other attendees to ensure organizer is always first
|
||||||
var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
|
var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
|
||||||
@@ -348,7 +363,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
{
|
{
|
||||||
// Capture original state BEFORE making any changes for potential revert
|
// Capture original state BEFORE making any changes for potential revert
|
||||||
var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id);
|
var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id);
|
||||||
var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.EventTrackingId);
|
var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.Id);
|
||||||
|
|
||||||
// Get selected reminder options
|
// Get selected reminder options
|
||||||
var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
|
var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
|
||||||
@@ -370,7 +385,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save reminders to database
|
// Save reminders to database
|
||||||
await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.EventTrackingId, newReminders);
|
await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.Id, newReminders);
|
||||||
Reminders = newReminders;
|
Reminders = newReminders;
|
||||||
|
|
||||||
// Update ShowAs if changed
|
// Update ShowAs if changed
|
||||||
@@ -499,7 +514,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
|||||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||||
|
|
||||||
// Reload attendees to get the updated status from the server
|
// Reload attendees to get the updated status from the server
|
||||||
await LoadAttendeesAsync(CurrentEvent.CalendarItem.EventTrackingId, CurrentEvent.CalendarItem);
|
await LoadAttendeesAsync(CurrentEvent.CalendarItem.Id, CurrentEvent.CalendarItem);
|
||||||
|
|
||||||
OnPropertyChanged(nameof(CurrentRsvpText));
|
OnPropertyChanged(nameof(CurrentRsvpText));
|
||||||
OnPropertyChanged(nameof(CurrentRsvpStatus));
|
OnPropertyChanged(nameof(CurrentRsvpStatus));
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
xmlns:abstract="using:Wino.Calendar.Views.Abstract"
|
xmlns:abstract="using:Wino.Calendar.Views.Abstract"
|
||||||
xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar"
|
xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar"
|
||||||
xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
|
xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
|
||||||
|
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
|
||||||
xmlns:coreControls="using:Wino.Core.UWP.Controls"
|
xmlns:coreControls="using:Wino.Core.UWP.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:data="using:Wino.Calendar.ViewModels.Data"
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:enums="using:Wino.Core.Domain.Enums"
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:local="using:Wino.Calendar.Views"
|
xmlns:local="using:Wino.Calendar.Views"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -43,6 +46,7 @@
|
|||||||
</Page.Resources>
|
</Page.Resources>
|
||||||
<Grid Padding="20">
|
<Grid Padding="20">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@@ -60,16 +64,16 @@
|
|||||||
DefaultLabelPosition="Right"
|
DefaultLabelPosition="Right"
|
||||||
IsSticky="True"
|
IsSticky="True"
|
||||||
OverflowButtonVisibility="Auto">
|
OverflowButtonVisibility="Auto">
|
||||||
<AppBarToggleButton
|
<AppBarButton
|
||||||
x:Name="ReadOnlyToggle"
|
Command="{x:Bind ViewModel.SaveCommand}"
|
||||||
Content="Read-only event"
|
Label="{x:Bind domain:Translator.Buttons_Save}">
|
||||||
IsChecked="True" />
|
|
||||||
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Save}">
|
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Icon="Save" />
|
<coreControls:WinoFontIcon Icon="Save" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
</AppBarButton>
|
</AppBarButton>
|
||||||
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Delete}">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.DeleteCommand}"
|
||||||
|
Label="{x:Bind domain:Translator.Buttons_Delete}">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Icon="Delete" />
|
<coreControls:WinoFontIcon Icon="Delete" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
@@ -78,7 +82,9 @@
|
|||||||
<AppBarSeparator />
|
<AppBarSeparator />
|
||||||
|
|
||||||
<!-- Join Online -->
|
<!-- Join Online -->
|
||||||
<AppBarButton Label="Join Online">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.JoinOnlineCommand}"
|
||||||
|
Label="Join Online">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Icon="EventJoinOnline" />
|
<coreControls:WinoFontIcon Icon="EventJoinOnline" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
@@ -86,27 +92,37 @@
|
|||||||
|
|
||||||
<AppBarSeparator />
|
<AppBarSeparator />
|
||||||
|
|
||||||
<!-- Join Options -->
|
<!-- RSVP Actions -->
|
||||||
<AppBarButton Label="Accept">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||||
|
CommandParameter="{x:Bind enums:AttendeeStatus.Accepted}"
|
||||||
|
Label="Accept">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Foreground="#527257" Icon="EventAccept" />
|
<coreControls:WinoFontIcon Foreground="#527257" Icon="EventAccept" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
</AppBarButton>
|
</AppBarButton>
|
||||||
|
|
||||||
<AppBarButton Label="Tentative">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||||
|
CommandParameter="{x:Bind enums:AttendeeStatus.Tentative}"
|
||||||
|
Label="Tentative">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventTentative" />
|
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventTentative" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
</AppBarButton>
|
</AppBarButton>
|
||||||
|
|
||||||
<AppBarButton Label="Decline">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||||
|
CommandParameter="{x:Bind enums:AttendeeStatus.Declined}"
|
||||||
|
Label="Decline">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<PathIcon Data="F1 M 10.253906 9.375 L 16.064453 15.185547 C 16.18815 15.309245 16.25 15.455729 16.25 15.625 C 16.25 15.794271 16.18815 15.940756 16.064453 16.064453 C 15.940754 16.188152 15.79427 16.25 15.625 16.25 C 15.455729 16.25 15.309244 16.188152 15.185547 16.064453 L 9.375 10.253906 L 3.564453 16.064453 C 3.440755 16.188152 3.294271 16.25 3.125 16.25 C 2.955729 16.25 2.809245 16.188152 2.685547 16.064453 C 2.561849 15.940756 2.5 15.794271 2.5 15.625 C 2.5 15.455729 2.561849 15.309245 2.685547 15.185547 L 8.496094 9.375 L 2.685547 3.564453 C 2.561849 3.440756 2.5 3.294271 2.5 3.125 C 2.5 2.95573 2.561849 2.809246 2.685547 2.685547 C 2.809245 2.56185 2.955729 2.5 3.125 2.5 C 3.294271 2.5 3.440755 2.56185 3.564453 2.685547 L 9.375 8.496094 L 15.185547 2.685547 C 15.309244 2.56185 15.455729 2.5 15.625 2.5 C 15.79427 2.5 15.940754 2.56185 16.064453 2.685547 C 16.18815 2.809246 16.25 2.95573 16.25 3.125 C 16.25 3.294271 16.18815 3.440756 16.064453 3.564453 Z " Foreground="#d94b4e" />
|
<PathIcon Data="F1 M 10.253906 9.375 L 16.064453 15.185547 C 16.18815 15.309245 16.25 15.455729 16.25 15.625 C 16.25 15.794271 16.18815 15.940756 16.064453 16.064453 C 15.940754 16.188152 15.79427 16.25 15.625 16.25 C 15.455729 16.25 15.309244 16.188152 15.185547 16.064453 L 9.375 10.253906 L 3.564453 16.064453 C 3.440755 16.188152 3.294271 16.25 3.125 16.25 C 2.955729 16.25 2.809245 16.188152 2.685547 16.064453 C 2.561849 15.940756 2.5 15.794271 2.5 15.625 C 2.5 15.455729 2.561849 15.309245 2.685547 15.185547 L 8.496094 9.375 L 2.685547 3.564453 C 2.561849 3.440756 2.5 3.294271 2.5 3.125 C 2.5 2.95573 2.561849 2.809246 2.685547 2.685547 C 2.809245 2.56185 2.955729 2.5 3.125 2.5 C 3.294271 2.5 3.440755 2.56185 3.564453 2.685547 L 9.375 8.496094 L 15.185547 2.685547 C 15.309244 2.56185 15.455729 2.5 15.625 2.5 C 15.79427 2.5 15.940754 2.56185 16.064453 2.685547 C 16.18815 2.809246 16.25 2.95573 16.25 3.125 C 16.25 3.294271 16.18815 3.440756 16.064453 3.564453 Z " Foreground="#d94b4e" />
|
||||||
|
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
</AppBarButton>
|
</AppBarButton>
|
||||||
|
|
||||||
<AppBarButton Label="Respond">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.ToggleRsvpPanelCommand}"
|
||||||
|
Label="{x:Bind ViewModel.CurrentRsvpText, Mode=OneWay}">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventRespond" />
|
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventRespond" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
@@ -118,7 +134,11 @@
|
|||||||
<AppBarElementContainer>
|
<AppBarElementContainer>
|
||||||
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
||||||
<TextBlock VerticalAlignment="Center" Text="Show as" />
|
<TextBlock VerticalAlignment="Center" Text="Show as" />
|
||||||
<ComboBox Width="150" />
|
<ComboBox
|
||||||
|
Width="150"
|
||||||
|
DisplayMemberPath="DisplayText"
|
||||||
|
ItemsSource="{x:Bind ViewModel.ShowAsOptions}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedShowAsOption, Mode=TwoWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</AppBarElementContainer>
|
</AppBarElementContainer>
|
||||||
|
|
||||||
@@ -127,14 +147,33 @@
|
|||||||
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
||||||
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
||||||
<TextBlock VerticalAlignment="Center" Text="Reminder" />
|
<TextBlock VerticalAlignment="Center" Text="Reminder" />
|
||||||
<ComboBox Width="150" />
|
<Button Content="Set">
|
||||||
|
<Button.Flyout>
|
||||||
|
<Flyout>
|
||||||
|
<ListView
|
||||||
|
Width="200"
|
||||||
|
MaxHeight="300"
|
||||||
|
ItemsSource="{x:Bind ViewModel.ReminderOptions}"
|
||||||
|
SelectionMode="None">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="calendarViewModels:ReminderOption">
|
||||||
|
<CheckBox Content="{x:Bind DisplayText}" IsChecked="{x:Bind IsSelected, Mode=TwoWay}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</Flyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</AppBarElementContainer>
|
</AppBarElementContainer>
|
||||||
|
|
||||||
<AppBarSeparator />
|
<AppBarSeparator />
|
||||||
|
|
||||||
<!-- Edit Series -->
|
<!-- Edit Series -->
|
||||||
<AppBarButton Label="Edit Series">
|
<AppBarButton
|
||||||
|
Command="{x:Bind ViewModel.ViewSeriesCommand}"
|
||||||
|
Label="Edit Series"
|
||||||
|
Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Icon="EventEditSeries" />
|
<coreControls:WinoFontIcon Icon="EventEditSeries" />
|
||||||
</AppBarButton.Icon>
|
</AppBarButton.Icon>
|
||||||
@@ -142,8 +181,51 @@
|
|||||||
</CommandBar>
|
</CommandBar>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- RSVP Panel -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||||
|
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="7"
|
||||||
|
Visibility="{x:Bind ViewModel.IsRsvpPanelVisible, Mode=OneWay}">
|
||||||
|
<Grid RowSpacing="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||||
|
CommandParameter="{x:Bind enums:AttendeeStatus.Accepted}"
|
||||||
|
Content="Accept" />
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||||
|
CommandParameter="{x:Bind enums:AttendeeStatus.Tentative}"
|
||||||
|
Content="Tentative" />
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||||
|
CommandParameter="{x:Bind enums:AttendeeStatus.Declined}"
|
||||||
|
Content="Decline" />
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.CloseRsvpPanelCommand}"
|
||||||
|
Content="Close" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
Grid.Row="1"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
PlaceholderText="Add a message (optional)"
|
||||||
|
Text="{x:Bind ViewModel.RsvpMessage, Mode=TwoWay}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Event details -->
|
<!-- Event details -->
|
||||||
<ScrollViewer Grid.Row="1">
|
<ScrollViewer Grid.Row="2">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="2*" />
|
<ColumnDefinition Width="2*" />
|
||||||
@@ -170,8 +252,7 @@
|
|||||||
<!-- Read-Only Event -->
|
<!-- Read-Only Event -->
|
||||||
<Grid
|
<Grid
|
||||||
x:Name="ReadOnlyDetailsGrid"
|
x:Name="ReadOnlyDetailsGrid"
|
||||||
RowSpacing="6"
|
RowSpacing="6">
|
||||||
Visibility="{x:Bind ReadOnlyToggle.IsChecked, Mode=OneWay}">
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="16" />
|
<RowDefinition Height="16" />
|
||||||
@@ -195,7 +276,7 @@
|
|||||||
<Grid
|
<Grid
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
ColumnSpacing="6"
|
ColumnSpacing="6"
|
||||||
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringEvent}">
|
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringEvent, Mode=OneWay}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
@@ -208,12 +289,6 @@
|
|||||||
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetRecurrenceString(ViewModel.CurrentEvent), Mode=OneWay}"
|
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetRecurrenceString(ViewModel.CurrentEvent), Mode=OneWay}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Editable Event -->
|
|
||||||
<Grid Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(ReadOnlyDetailsGrid.Visibility), Mode=OneWay}">
|
|
||||||
<TextBlock Text="editing" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -238,7 +313,8 @@
|
|||||||
<AutoSuggestBox
|
<AutoSuggestBox
|
||||||
Margin="6,0"
|
Margin="6,0"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
PlaceholderText="Invite someone" />
|
PlaceholderText="Invite someone"
|
||||||
|
Visibility="{x:Bind ViewModel.IsCurrentUserOrganizer, Mode=OneWay}" />
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@@ -251,6 +327,7 @@
|
|||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<PersonPicture
|
<PersonPicture
|
||||||
@@ -285,6 +362,19 @@
|
|||||||
Text="{x:Bind domain:Translator.CalendarEventDetails_Organizer}" />
|
Text="{x:Bind domain:Translator.CalendarEventDetails_Organizer}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Grid.Column="2"
|
||||||
|
Padding="6,2"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Background="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
CornerRadius="4">
|
||||||
|
<TextBlock
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{x:Bind AttendenceStatus}" />
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListView.ItemTemplate>
|
</ListView.ItemTemplate>
|
||||||
@@ -296,7 +386,8 @@
|
|||||||
<Grid
|
<Grid
|
||||||
x:Name="AttachmentsGrid"
|
x:Name="AttachmentsGrid"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Style="{StaticResource EventDetailsPanelGridStyle}">
|
Style="{StaticResource EventDetailsPanelGridStyle}"
|
||||||
|
Visibility="{x:Bind ViewModel.HasAttachments, Mode=OneWay}">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
@@ -304,6 +395,82 @@
|
|||||||
|
|
||||||
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="Attachments" />
|
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="Attachments" />
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
Grid.Row="1"
|
||||||
|
IsItemClickEnabled="True"
|
||||||
|
ItemClick="AttachmentClicked"
|
||||||
|
ItemsSource="{x:Bind ViewModel.Attachments, Mode=OneWay}"
|
||||||
|
SelectionMode="None">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="data:CalendarAttachmentViewModel">
|
||||||
|
<Grid Height="51">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="50" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid
|
||||||
|
Grid.Row="0"
|
||||||
|
Height="50"
|
||||||
|
Background="Transparent"
|
||||||
|
ColumnSpacing="6">
|
||||||
|
<Grid.ContextFlyout>
|
||||||
|
<MenuFlyout Placement="Right">
|
||||||
|
<MenuFlyoutItem
|
||||||
|
Click="OpenCalendarAttachment_Click"
|
||||||
|
CommandParameter="{x:Bind}"
|
||||||
|
Text="{x:Bind domain:Translator.Buttons_Open}" />
|
||||||
|
<MenuFlyoutItem
|
||||||
|
Click="SaveCalendarAttachment_Click"
|
||||||
|
CommandParameter="{x:Bind}"
|
||||||
|
Text="{x:Bind domain:Translator.Buttons_Save}" />
|
||||||
|
</MenuFlyout>
|
||||||
|
</Grid.ContextFlyout>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="24" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<SymbolIcon Symbol="Attach" />
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Grid.Column="1"
|
||||||
|
MaxWidth="200"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
FontSize="13"
|
||||||
|
MaxLines="1"
|
||||||
|
Text="{x:Bind FileName}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="Gray"
|
||||||
|
Text="{x:Bind ReadableSize}" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<ProgressBar
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,-5,0,0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
IsIndeterminate="{x:Bind IsBusy, Mode=OneWay}"
|
||||||
|
ShowError="False"
|
||||||
|
ShowPaused="False"
|
||||||
|
Visibility="{x:Bind IsBusy, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Wino.Calendar.ViewModels.Data;
|
||||||
using Wino.Calendar.Views.Abstract;
|
using Wino.Calendar.Views.Abstract;
|
||||||
|
|
||||||
namespace Wino.Calendar.Views;
|
namespace Wino.Calendar.Views;
|
||||||
@@ -9,4 +10,28 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract
|
|||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AttachmentClicked(object sender, ItemClickEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.ClickedItem is CalendarAttachmentViewModel attachmentViewModel)
|
||||||
|
{
|
||||||
|
ViewModel?.OpenAttachmentCommand.Execute(attachmentViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenCalendarAttachment_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is MenuFlyoutItem item && item.CommandParameter is CalendarAttachmentViewModel attachment)
|
||||||
|
{
|
||||||
|
ViewModel?.OpenAttachmentCommand.Execute(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveCalendarAttachment_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is MenuFlyoutItem item && item.CommandParameter is CalendarAttachmentViewModel attachment)
|
||||||
|
{
|
||||||
|
ViewModel?.SaveAttachmentCommand.Execute(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Calendar;
|
namespace Wino.Core.Domain.Entities.Calendar;
|
||||||
@@ -15,7 +16,9 @@ public class AccountCalendar : IAccountCalendar
|
|||||||
public string SynchronizationDeltaToken { get; set; }
|
public string SynchronizationDeltaToken { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool IsPrimary { get; set; }
|
public bool IsPrimary { get; set; }
|
||||||
|
public bool IsSynchronizationEnabled { get; set; } = true;
|
||||||
public bool IsExtended { get; set; } = true;
|
public bool IsExtended { get; set; } = true;
|
||||||
|
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unused for now.
|
/// Unused for now.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates the source of a calendar item update.
|
||||||
|
/// </summary>
|
||||||
|
public enum CalendarItemUpdateSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from client-side UI changes (ApplyUIChanges).
|
||||||
|
/// </summary>
|
||||||
|
ClientUpdated,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from client-side UI revert (RevertUIChanges).
|
||||||
|
/// </summary>
|
||||||
|
ClientReverted,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update originated from server synchronization or database operations.
|
||||||
|
/// </summary>
|
||||||
|
Server
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -9,9 +10,11 @@ public interface IAccountCalendar
|
|||||||
string TextColorHex { get; set; }
|
string TextColorHex { get; set; }
|
||||||
string BackgroundColorHex { get; set; }
|
string BackgroundColorHex { get; set; }
|
||||||
bool IsPrimary { get; set; }
|
bool IsPrimary { get; set; }
|
||||||
|
bool IsSynchronizationEnabled { get; set; }
|
||||||
Guid AccountId { get; set; }
|
Guid AccountId { get; set; }
|
||||||
string RemoteCalendarId { get; set; }
|
string RemoteCalendarId { get; set; }
|
||||||
bool IsExtended { get; set; }
|
bool IsExtended { get; set; }
|
||||||
|
CalendarItemShowAs DefaultShowAs { get; set; }
|
||||||
Guid Id { get; set; }
|
Guid Id { get; set; }
|
||||||
MailAccount MailAccount { get; set; }
|
MailAccount MailAccount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using System;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Wino.Core.Domain.Entities.Calendar;
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Messaging.Client.Calendar;
|
using Wino.Messaging.Client.Calendar;
|
||||||
|
|
||||||
namespace Wino.Core.ViewModels;
|
namespace Wino.Core.ViewModels;
|
||||||
@@ -9,14 +11,19 @@ public class CalendarBaseViewModel : CoreBaseViewModel,
|
|||||||
IRecipient<CalendarItemUpdated>,
|
IRecipient<CalendarItemUpdated>,
|
||||||
IRecipient<CalendarItemDeleted>
|
IRecipient<CalendarItemDeleted>
|
||||||
{
|
{
|
||||||
public void Receive(CalendarItemAdded message) => OnCalendarItemAdded(message.CalendarItem);
|
public void Receive(CalendarItemAdded message) => DispatchToUIThread(() => OnCalendarItemAdded(message.CalendarItem));
|
||||||
public void Receive(CalendarItemUpdated message) => OnCalendarItemUpdated(message.CalendarItem);
|
public void Receive(CalendarItemUpdated message) => DispatchToUIThread(() => OnCalendarItemUpdated(message.CalendarItem, message.Source));
|
||||||
public void Receive(CalendarItemDeleted message) => OnCalendarItemDeleted(message.CalendarItem);
|
public void Receive(CalendarItemDeleted message) => DispatchToUIThread(() => OnCalendarItemDeleted(message.CalendarItem));
|
||||||
|
|
||||||
protected virtual void OnCalendarItemAdded(CalendarItem calendarItem) { }
|
protected virtual void OnCalendarItemAdded(CalendarItem calendarItem) { }
|
||||||
protected virtual void OnCalendarItemUpdated(CalendarItem calendarItem) { }
|
protected virtual void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) { }
|
||||||
protected virtual void OnCalendarItemDeleted(CalendarItem calendarItem) { }
|
protected virtual void OnCalendarItemDeleted(CalendarItem calendarItem) { }
|
||||||
|
|
||||||
|
private void DispatchToUIThread(Action action)
|
||||||
|
{
|
||||||
|
_ = ExecuteUIThread(action);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void RegisterRecipients()
|
protected override void RegisterRecipients()
|
||||||
{
|
{
|
||||||
base.RegisterRecipients();
|
base.RegisterRecipients();
|
||||||
@@ -35,3 +42,4 @@ public class CalendarBaseViewModel : CoreBaseViewModel,
|
|||||||
Messenger.Unregister<CalendarItemDeleted>(this);
|
Messenger.Unregister<CalendarItemDeleted>(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,18 @@ public class CoreBaseViewModel : ObservableRecipient, INavigationAware
|
|||||||
|
|
||||||
public virtual void OnPageLoaded() { }
|
public virtual void OnPageLoaded() { }
|
||||||
|
|
||||||
public async Task ExecuteUIThread(Action action) => await Dispatcher?.ExecuteOnUIThread(action);
|
public Task ExecuteUIThread(Action action)
|
||||||
|
{
|
||||||
|
if (action == null) return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (Dispatcher == null)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dispatcher.ExecuteOnUIThread(action);
|
||||||
|
}
|
||||||
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage => Messenger.Send(message);
|
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage => Messenger.Send(message);
|
||||||
|
|
||||||
protected virtual void OnDispatcherAssigned() { }
|
protected virtual void OnDispatcherAssigned() { }
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ public static class GoogleIntegratorExtensions
|
|||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
TimeZone = calendarListEntry.TimeZone,
|
TimeZone = calendarListEntry.TimeZone,
|
||||||
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
||||||
|
IsSynchronizationEnabled = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bg color must present. Generate one if doesnt exists.
|
// Bg color must present. Generate one if doesnt exists.
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ public static class OutlookIntegratorExtensions
|
|||||||
RemoteCalendarId = outlookCalendar.Id,
|
RemoteCalendarId = outlookCalendar.Id,
|
||||||
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
|
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
|
||||||
Name = outlookCalendar.Name,
|
Name = outlookCalendar.Name,
|
||||||
|
IsSynchronizationEnabled = true,
|
||||||
IsExtended = true,
|
IsExtended = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ public record AcceptEventRequest(CalendarItem Item, string ResponseMessage = nul
|
|||||||
Item.Status = CalendarItemStatus.Accepted;
|
Item.Status = CalendarItemStatus.Accepted;
|
||||||
|
|
||||||
// Notify UI that the event status was updated
|
// Notify UI that the event status was updated
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RevertUIChanges()
|
public override void RevertUIChanges()
|
||||||
{
|
{
|
||||||
// If acceptance fails, revert to the previous status
|
// If acceptance fails, revert to the previous status
|
||||||
Item.Status = _previousStatus;
|
Item.Status = _previousStatus;
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ public record DeclineEventRequest(CalendarItem Item, string ResponseMessage = nu
|
|||||||
Item.Status = CalendarItemStatus.Cancelled;
|
Item.Status = CalendarItemStatus.Cancelled;
|
||||||
|
|
||||||
// Notify UI that the event status was updated
|
// Notify UI that the event status was updated
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RevertUIChanges()
|
public override void RevertUIChanges()
|
||||||
{
|
{
|
||||||
// If decline fails, revert to the previous status
|
// If decline fails, revert to the previous status
|
||||||
Item.Status = _previousStatus;
|
Item.Status = _previousStatus;
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ public record TentativeEventRequest(CalendarItem Item, string ResponseMessage =
|
|||||||
Item.Status = CalendarItemStatus.Tentative;
|
Item.Status = CalendarItemStatus.Tentative;
|
||||||
|
|
||||||
// Notify UI that the event status was updated
|
// Notify UI that the event status was updated
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RevertUIChanges()
|
public override void RevertUIChanges()
|
||||||
{
|
{
|
||||||
// If tentative acceptance fails, revert to the previous status
|
// If tentative acceptance fails, revert to the previous status
|
||||||
Item.Status = _previousStatus;
|
Item.Status = _previousStatus;
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public record UpdateCalendarEventRequest(CalendarItem Item, List<CalendarEventAt
|
|||||||
public override void ApplyUIChanges()
|
public override void ApplyUIChanges()
|
||||||
{
|
{
|
||||||
// Notify UI that the event was updated locally
|
// Notify UI that the event was updated locally
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RevertUIChanges()
|
public override void RevertUIChanges()
|
||||||
@@ -42,12 +42,12 @@ public record UpdateCalendarEventRequest(CalendarItem Item, List<CalendarEventAt
|
|||||||
if (OriginalItem != null && OriginalAttendees != null)
|
if (OriginalItem != null && OriginalAttendees != null)
|
||||||
{
|
{
|
||||||
// Send the original item back to restore UI state
|
// Send the original item back to restore UI state
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(OriginalItem));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(OriginalItem, CalendarItemUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback: just notify with current item to trigger refresh
|
// Fallback: just notify with current item to trigger refresh
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(Item, CalendarItemUpdateSource.ClientReverted));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,7 +484,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
_logger.Debug("Is initial synchronization: {IsInitialSync}", isInitialSync);
|
_logger.Debug("Is initial synchronization: {IsInitialSync}", isInitialSync);
|
||||||
|
|
||||||
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
var localCalendars = (await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false))
|
||||||
|
.Where(c => c.IsSynchronizationEnabled)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// TODO: Better logging and exception handling.
|
// TODO: Better logging and exception handling.
|
||||||
foreach (var calendar in localCalendars)
|
foreach (var calendar in localCalendars)
|
||||||
@@ -566,6 +568,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||||
|
var remotePrimaryCalendarId = GetPrimaryCalendarId(calendarListResponse.Items);
|
||||||
|
|
||||||
List<AccountCalendar> insertedCalendars = new();
|
List<AccountCalendar> insertedCalendars = new();
|
||||||
List<AccountCalendar> updatedCalendars = new();
|
List<AccountCalendar> updatedCalendars = new();
|
||||||
@@ -596,14 +599,21 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
{
|
{
|
||||||
// Insert new calendar.
|
// Insert new calendar.
|
||||||
var localCalendar = calendar.AsCalendar(Account.Id);
|
var localCalendar = calendar.AsCalendar(Account.Id);
|
||||||
|
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
insertedCalendars.Add(localCalendar);
|
insertedCalendars.Add(localCalendar);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update existing calendar. Right now we only update the name.
|
// Update existing calendar. Right now we only update the name.
|
||||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar))
|
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId))
|
||||||
{
|
{
|
||||||
existingLocalCalendar.Name = calendar.Summary;
|
existingLocalCalendar.Name = calendar.Summary;
|
||||||
|
existingLocalCalendar.TimeZone = calendar.TimeZone;
|
||||||
|
if (!string.IsNullOrEmpty(calendar.BackgroundColor))
|
||||||
|
existingLocalCalendar.BackgroundColorHex = calendar.BackgroundColor;
|
||||||
|
if (!string.IsNullOrEmpty(calendar.ForegroundColor))
|
||||||
|
existingLocalCalendar.TextColorHex = calendar.ForegroundColor;
|
||||||
|
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
updatedCalendars.Add(existingLocalCalendar);
|
updatedCalendars.Add(existingLocalCalendar);
|
||||||
}
|
}
|
||||||
@@ -770,14 +780,41 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldUpdateCalendar(CalendarListEntry calendarListEntry, AccountCalendar accountCalendar)
|
private bool ShouldUpdateCalendar(CalendarListEntry calendarListEntry, AccountCalendar accountCalendar, string remotePrimaryCalendarId)
|
||||||
{
|
{
|
||||||
// TODO: Only calendar name is updated for now. We can add more checks here.
|
|
||||||
|
|
||||||
var remoteCalendarName = calendarListEntry.Summary;
|
var remoteCalendarName = calendarListEntry.Summary;
|
||||||
var localCalendarName = accountCalendar.Name;
|
var remoteTimeZone = calendarListEntry.TimeZone;
|
||||||
|
var remoteBackgroundColor = string.IsNullOrEmpty(calendarListEntry.BackgroundColor) ? accountCalendar.BackgroundColorHex : calendarListEntry.BackgroundColor;
|
||||||
|
var remoteTextColor = string.IsNullOrEmpty(calendarListEntry.ForegroundColor) ? accountCalendar.TextColorHex : calendarListEntry.ForegroundColor;
|
||||||
|
var remoteIsPrimary = string.Equals(calendarListEntry.Id, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
bool isNameChanged = !string.Equals(accountCalendar.Name, remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isTimeZoneChanged = !string.Equals(accountCalendar.TimeZone, remoteTimeZone, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isBackgroundColorChanged = !string.Equals(accountCalendar.BackgroundColorHex, remoteBackgroundColor, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isTextColorChanged = !string.Equals(accountCalendar.TextColorHex, remoteTextColor, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isPrimaryChanged = accountCalendar.IsPrimary != remoteIsPrimary;
|
||||||
|
|
||||||
|
return isNameChanged || isTimeZoneChanged || isBackgroundColorChanged || isTextColorChanged || isPrimaryChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPrimaryCalendarId(IList<CalendarListEntry> remoteCalendars)
|
||||||
|
{
|
||||||
|
if (remoteCalendars == null || remoteCalendars.Count == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var explicitPrimary = remoteCalendars.FirstOrDefault(c => c.Primary.GetValueOrDefault());
|
||||||
|
if (explicitPrimary != null)
|
||||||
|
return explicitPrimary.Id;
|
||||||
|
|
||||||
|
var byPrimaryKeyword = remoteCalendars.FirstOrDefault(c => string.Equals(c.Id, "primary", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (byPrimaryKeyword != null)
|
||||||
|
return byPrimaryKeyword.Id;
|
||||||
|
|
||||||
|
var byAccountAddress = remoteCalendars.FirstOrDefault(c => string.Equals(c.Id, Account.Address, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (byAccountAddress != null)
|
||||||
|
return byAccountAddress.Id;
|
||||||
|
|
||||||
|
return remoteCalendars.First().Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldUpdateFolder(Label remoteFolder, MailItemFolder existingLocalFolder)
|
private bool ShouldUpdateFolder(Label remoteFolder, MailItemFolder existingLocalFolder)
|
||||||
|
|||||||
@@ -1961,7 +1961,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
|
|
||||||
await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false);
|
await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
var localCalendars = (await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false))
|
||||||
|
.Where(c => c.IsSynchronizationEnabled)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse eventsDeltaResponse = null;
|
Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse eventsDeltaResponse = null;
|
||||||
|
|
||||||
@@ -2078,6 +2080,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
|
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var calendars = await _graphClient.Me.Calendars.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
var calendars = await _graphClient.Me.Calendars.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
var remotePrimaryCalendarId = await GetPrimaryCalendarIdAsync(calendars.Value, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -2110,14 +2113,18 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
{
|
{
|
||||||
// Insert new calendar.
|
// Insert new calendar.
|
||||||
var localCalendar = calendar.AsCalendar(Account);
|
var localCalendar = calendar.AsCalendar(Account);
|
||||||
|
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
insertedCalendars.Add(localCalendar);
|
insertedCalendars.Add(localCalendar);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update existing calendar. Right now we only update the name.
|
// Update existing calendar. Right now we only update the name.
|
||||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar))
|
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId))
|
||||||
{
|
{
|
||||||
existingLocalCalendar.Name = calendar.Name;
|
existingLocalCalendar.Name = calendar.Name;
|
||||||
|
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!string.IsNullOrEmpty(calendar.HexColor))
|
||||||
|
existingLocalCalendar.BackgroundColorHex = calendar.HexColor;
|
||||||
|
|
||||||
updatedCalendars.Add(existingLocalCalendar);
|
updatedCalendars.Add(existingLocalCalendar);
|
||||||
}
|
}
|
||||||
@@ -2147,14 +2154,40 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldUpdateCalendar(Calendar calendar, AccountCalendar accountCalendar)
|
private bool ShouldUpdateCalendar(Calendar calendar, AccountCalendar accountCalendar, string remotePrimaryCalendarId)
|
||||||
{
|
{
|
||||||
// TODO: Only calendar name is updated for now. We can add more checks here.
|
|
||||||
|
|
||||||
var remoteCalendarName = calendar.Name;
|
var remoteCalendarName = calendar.Name;
|
||||||
var localCalendarName = accountCalendar.Name;
|
var remoteBackgroundColor = string.IsNullOrEmpty(calendar.HexColor) ? accountCalendar.BackgroundColorHex : calendar.HexColor;
|
||||||
|
var remoteIsPrimary = string.Equals(calendar.Id, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
bool isNameChanged = !string.Equals(accountCalendar.Name, remoteCalendarName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isBackgroundColorChanged = !string.Equals(accountCalendar.BackgroundColorHex, remoteBackgroundColor, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isPrimaryChanged = accountCalendar.IsPrimary != remoteIsPrimary;
|
||||||
|
|
||||||
|
return isNameChanged || isBackgroundColorChanged || isPrimaryChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetPrimaryCalendarIdAsync(IList<Calendar> remoteCalendars, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (remoteCalendars == null || remoteCalendars.Count == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var explicitPrimary = remoteCalendars.FirstOrDefault(c => c.IsDefaultCalendar.GetValueOrDefault());
|
||||||
|
if (explicitPrimary != null)
|
||||||
|
return explicitPrimary.Id;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var meCalendar = await _graphClient.Me.Calendar.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrEmpty(meCalendar?.Id))
|
||||||
|
return meCalendar.Id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warning(ex, "Failed to fetch default Outlook calendar for {Name}. Falling back to first available calendar.", Account.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteCalendars.First().Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Calendar Operations
|
#region Calendar Operations
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
private readonly ICalendarService _calendarService;
|
private readonly ICalendarService _calendarService;
|
||||||
|
private readonly IStatePersistanceService _statePersistanceService;
|
||||||
private bool isLoaded = false;
|
private bool isLoaded = false;
|
||||||
|
|
||||||
public MailAccount Account { get; set; }
|
public MailAccount Account { get; set; }
|
||||||
@@ -60,12 +61,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
public AccountDetailsPageViewModel(IMailDialogService dialogService,
|
public AccountDetailsPageViewModel(IMailDialogService dialogService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
IFolderService folderService,
|
IFolderService folderService,
|
||||||
ICalendarService calendarService)
|
ICalendarService calendarService,
|
||||||
|
IStatePersistanceService statePersistanceService)
|
||||||
{
|
{
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_folderService = folderService;
|
_folderService = folderService;
|
||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
|
_statePersistanceService = statePersistanceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -132,6 +135,8 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
|
|
||||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||||
|
|
||||||
|
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1;
|
||||||
|
|
||||||
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
|
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
|
||||||
|
|
||||||
foreach (var folder in folderStructures)
|
foreach (var folder in folderStructures)
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
existingCalendar.BackgroundColorHex = accountCalendar.BackgroundColorHex;
|
existingCalendar.BackgroundColorHex = accountCalendar.BackgroundColorHex;
|
||||||
existingCalendar.IsExtended = accountCalendar.IsExtended;
|
existingCalendar.IsExtended = accountCalendar.IsExtended;
|
||||||
existingCalendar.IsPrimary = accountCalendar.IsPrimary;
|
existingCalendar.IsPrimary = accountCalendar.IsPrimary;
|
||||||
|
existingCalendar.IsSynchronizationEnabled = accountCalendar.IsSynchronizationEnabled;
|
||||||
|
existingCalendar.DefaultShowAs = accountCalendar.DefaultShowAs;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -241,6 +243,8 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
existingCalendar.BackgroundColorHex = accountCalendar.BackgroundColorHex;
|
existingCalendar.BackgroundColorHex = accountCalendar.BackgroundColorHex;
|
||||||
existingCalendar.IsExtended = accountCalendar.IsExtended;
|
existingCalendar.IsExtended = accountCalendar.IsExtended;
|
||||||
existingCalendar.IsPrimary = accountCalendar.IsPrimary;
|
existingCalendar.IsPrimary = accountCalendar.IsPrimary;
|
||||||
|
existingCalendar.IsSynchronizationEnabled = accountCalendar.IsSynchronizationEnabled;
|
||||||
|
existingCalendar.DefaultShowAs = accountCalendar.DefaultShowAs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,13 @@ public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract
|
|||||||
|
|
||||||
if (e.NavigationMode == NavigationMode.New)
|
if (e.NavigationMode == NavigationMode.New)
|
||||||
{
|
{
|
||||||
// Set initial tab to Mail (index 1)
|
var targetTabIndex = ViewModel.SelectedTabIndex;
|
||||||
TabSelector.SelectedItem = TabSelector.Items[1];
|
if (targetTabIndex < 0 || targetTabIndex >= TabSelector.Items.Count)
|
||||||
|
{
|
||||||
|
targetTabIndex = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TabSelector.SelectedItem = TabSelector.Items[targetTabIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Navigation;
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
@@ -77,6 +78,12 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract,
|
|||||||
|
|
||||||
private async Task RenderDescriptionAsync()
|
private async Task RenderDescriptionAsync()
|
||||||
{
|
{
|
||||||
|
if (DispatcherQueue != null && !DispatcherQueue.HasThreadAccess)
|
||||||
|
{
|
||||||
|
await DispatcherQueue.EnqueueAsync(RenderDescriptionAsync);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ViewModel?.CurrentEvent?.CalendarItem == null)
|
if (ViewModel?.CurrentEvent?.CalendarItem == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -141,6 +148,12 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract,
|
|||||||
|
|
||||||
private async Task UpdateEditorThemeAsync()
|
private async Task UpdateEditorThemeAsync()
|
||||||
{
|
{
|
||||||
|
if (DispatcherQueue != null && !DispatcherQueue.HasThreadAccess)
|
||||||
|
{
|
||||||
|
await DispatcherQueue.EnqueueAsync(UpdateEditorThemeAsync);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await DOMLoadedTask.Task;
|
await DOMLoadedTask.Task;
|
||||||
|
|
||||||
if (ViewModel.IsDarkWebviewRenderer)
|
if (ViewModel.IsDarkWebviewRenderer)
|
||||||
@@ -171,9 +184,9 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract,
|
|||||||
_ = UpdateEditorThemeAsync();
|
_ = UpdateEditorThemeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async void IRecipient<CalendarDescriptionRenderingRequested>.Receive(CalendarDescriptionRenderingRequested message)
|
void IRecipient<CalendarDescriptionRenderingRequested>.Receive(CalendarDescriptionRenderingRequested message)
|
||||||
{
|
{
|
||||||
await RenderDescriptionAsync();
|
_ = RenderDescriptionAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RegisterRecipients()
|
protected override void RegisterRecipients()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using Wino.Core.Domain.Entities.Calendar;
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Messaging.Client.Calendar;
|
namespace Wino.Messaging.Client.Calendar;
|
||||||
|
|
||||||
public record CalendarItemAdded(CalendarItem CalendarItem);
|
public record CalendarItemAdded(CalendarItem CalendarItem);
|
||||||
public record CalendarItemUpdated(CalendarItem CalendarItem);
|
public record CalendarItemUpdated(CalendarItem CalendarItem, CalendarItemUpdateSource Source);
|
||||||
public record CalendarItemDeleted(CalendarItem CalendarItem);
|
public record CalendarItemDeleted(CalendarItem CalendarItem);
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public class CalendarService : BaseDatabaseService, ICalendarService
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(calendarItem));
|
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(calendarItem, CalendarItemUpdateSource.Server));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,4 +64,5 @@ public class DatabaseService : IDatabaseService
|
|||||||
Connection.CreateTableAsync<MailInvitationCalendarMapping>()
|
Connection.CreateTableAsync<MailInvitationCalendarMapping>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user