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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Load account
|
||||
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)
|
||||
if (Account == null || AccountCalendar == null)
|
||||
return;
|
||||
|
||||
// Initialize properties from AccountCalendar
|
||||
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
||||
IsSyncEnabled = AccountCalendar.IsExtended;
|
||||
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
|
||||
IsPrimaryCalendar = AccountCalendar.IsPrimary;
|
||||
|
||||
// TODO: Default ShowAs is not stored in AccountCalendar yet, defaulting to Busy
|
||||
SelectedDefaultShowAsOption = ShowAsOptions[2]; // Busy
|
||||
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
|
||||
}
|
||||
|
||||
partial void OnAccountColorHexChanged(string value)
|
||||
@@ -98,7 +99,7 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
||||
{
|
||||
if (AccountCalendar != null)
|
||||
{
|
||||
AccountCalendar.IsExtended = value;
|
||||
AccountCalendar.IsSynchronizationEnabled = value;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -114,11 +115,10 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
||||
|
||||
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
||||
{
|
||||
// TODO: Default ShowAs should be stored in AccountCalendar or account preferences
|
||||
// For now, this is just a placeholder as the property doesn't exist yet
|
||||
if (value != null)
|
||||
if (AccountCalendar != null && value != null)
|
||||
{
|
||||
// Future: Store value.ShowAs somewhere
|
||||
AccountCalendar.DefaultShowAs = value.ShowAs;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
Description = string.Empty,
|
||||
Location = Location ?? string.Empty,
|
||||
Title = EventName,
|
||||
ShowAs = SelectedQuickEventAccountCalendar.DefaultShowAs,
|
||||
IsHidden = false,
|
||||
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}");
|
||||
|
||||
// Series master events should not be visible on the UI.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public bool IsSynchronizationEnabled
|
||||
{
|
||||
get => AccountCalendar.IsSynchronizationEnabled;
|
||||
set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i);
|
||||
}
|
||||
|
||||
public Guid AccountId
|
||||
{
|
||||
get => AccountCalendar.AccountId;
|
||||
@@ -65,6 +72,12 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend
|
||||
get => AccountCalendar.RemoteCalendarId;
|
||||
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 MailAccount MailAccount { get => MailAccount; set => MailAccount = value; }
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
||||
public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))]
|
||||
public partial string RsvpMessage { get; set; } = string.Empty;
|
||||
|
||||
public ObservableCollection<RsvpStatusOption> RsvpStatusOptions { get; } = new ObservableCollection<RsvpStatusOption>();
|
||||
@@ -129,7 +130,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
||||
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
|
||||
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
|
||||
CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
|
||||
_ => throw new NotImplementedException()
|
||||
_ => Translator.CalendarEventResponse_NotResponded
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -179,19 +180,33 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
||||
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 (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);
|
||||
if (refreshedEvent != null)
|
||||
{
|
||||
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);
|
||||
|
||||
await LoadAttendeesAsync(currentEventItem.EventTrackingId, currentEventItem);
|
||||
await LoadAttendeesAsync(currentEventItem.Id, currentEventItem);
|
||||
|
||||
// Initialize SelectedShowAsOption based on current event's ShowAs
|
||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2];
|
||||
|
||||
// Load reminders for this calendar item
|
||||
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.EventTrackingId);
|
||||
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id);
|
||||
InitializeReminderOptions();
|
||||
|
||||
// Load attachments
|
||||
await LoadAttachmentsAsync(currentEventItem.EventTrackingId);
|
||||
await LoadAttachmentsAsync(currentEventItem.Id);
|
||||
}
|
||||
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();
|
||||
|
||||
var attendees = await _calendarService.GetAttendeesAsync(eventTrackingId);
|
||||
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
|
||||
|
||||
// Separate organizer from other attendees to ensure organizer is always first
|
||||
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
|
||||
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
|
||||
var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
|
||||
@@ -370,7 +385,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
||||
}
|
||||
|
||||
// Save reminders to database
|
||||
await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.EventTrackingId, newReminders);
|
||||
await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.Id, newReminders);
|
||||
Reminders = newReminders;
|
||||
|
||||
// Update ShowAs if changed
|
||||
@@ -499,7 +514,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
|
||||
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
|
||||
|
||||
// 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(CurrentRsvpStatus));
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
xmlns:abstract="using:Wino.Calendar.Views.Abstract"
|
||||
xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar"
|
||||
xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
|
||||
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
|
||||
xmlns:coreControls="using:Wino.Core.UWP.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Wino.Calendar.ViewModels.Data"
|
||||
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"
|
||||
@@ -43,6 +46,7 @@
|
||||
</Page.Resources>
|
||||
<Grid Padding="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
@@ -60,16 +64,16 @@
|
||||
DefaultLabelPosition="Right"
|
||||
IsSticky="True"
|
||||
OverflowButtonVisibility="Auto">
|
||||
<AppBarToggleButton
|
||||
x:Name="ReadOnlyToggle"
|
||||
Content="Read-only event"
|
||||
IsChecked="True" />
|
||||
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Save}">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.SaveCommand}"
|
||||
Label="{x:Bind domain:Translator.Buttons_Save}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="Save" />
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Delete}">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.DeleteCommand}"
|
||||
Label="{x:Bind domain:Translator.Buttons_Delete}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="Delete" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -78,7 +82,9 @@
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Join Online -->
|
||||
<AppBarButton Label="Join Online">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.JoinOnlineCommand}"
|
||||
Label="Join Online">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="EventJoinOnline" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -86,27 +92,37 @@
|
||||
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Join Options -->
|
||||
<AppBarButton Label="Accept">
|
||||
<!-- RSVP Actions -->
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||
CommandParameter="{x:Bind enums:AttendeeStatus.Accepted}"
|
||||
Label="Accept">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Foreground="#527257" Icon="EventAccept" />
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
|
||||
<AppBarButton Label="Tentative">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||
CommandParameter="{x:Bind enums:AttendeeStatus.Tentative}"
|
||||
Label="Tentative">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventTentative" />
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
|
||||
<AppBarButton Label="Decline">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.SendRsvpResponseCommand}"
|
||||
CommandParameter="{x:Bind enums:AttendeeStatus.Declined}"
|
||||
Label="Decline">
|
||||
<AppBarButton.Icon>
|
||||
<PathIcon Data="F1 M 10.253906 9.375 L 16.064453 15.185547 C 16.18815 15.309245 16.25 15.455729 16.25 15.625 C 16.25 15.794271 16.18815 15.940756 16.064453 16.064453 C 15.940754 16.188152 15.79427 16.25 15.625 16.25 C 15.455729 16.25 15.309244 16.188152 15.185547 16.064453 L 9.375 10.253906 L 3.564453 16.064453 C 3.440755 16.188152 3.294271 16.25 3.125 16.25 C 2.955729 16.25 2.809245 16.188152 2.685547 16.064453 C 2.561849 15.940756 2.5 15.794271 2.5 15.625 C 2.5 15.455729 2.561849 15.309245 2.685547 15.185547 L 8.496094 9.375 L 2.685547 3.564453 C 2.561849 3.440756 2.5 3.294271 2.5 3.125 C 2.5 2.95573 2.561849 2.809246 2.685547 2.685547 C 2.809245 2.56185 2.955729 2.5 3.125 2.5 C 3.294271 2.5 3.440755 2.56185 3.564453 2.685547 L 9.375 8.496094 L 15.185547 2.685547 C 15.309244 2.56185 15.455729 2.5 15.625 2.5 C 15.79427 2.5 15.940754 2.56185 16.064453 2.685547 C 16.18815 2.809246 16.25 2.95573 16.25 3.125 C 16.25 3.294271 16.18815 3.440756 16.064453 3.564453 Z " Foreground="#d94b4e" />
|
||||
|
||||
</AppBarButton.Icon>
|
||||
</AppBarButton>
|
||||
|
||||
<AppBarButton Label="Respond">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.ToggleRsvpPanelCommand}"
|
||||
Label="{x:Bind ViewModel.CurrentRsvpText, Mode=OneWay}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventRespond" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -118,7 +134,11 @@
|
||||
<AppBarElementContainer>
|
||||
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
||||
<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>
|
||||
</AppBarElementContainer>
|
||||
|
||||
@@ -127,14 +147,33 @@
|
||||
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
|
||||
<coreControls:WinoFontIcon FontSize="16" Icon="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>
|
||||
</AppBarElementContainer>
|
||||
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Edit Series -->
|
||||
<AppBarButton Label="Edit Series">
|
||||
<AppBarButton
|
||||
Command="{x:Bind ViewModel.ViewSeriesCommand}"
|
||||
Label="Edit Series"
|
||||
Visibility="{x:Bind ViewModel.CanEditSeries, Mode=OneWay}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="EventEditSeries" />
|
||||
</AppBarButton.Icon>
|
||||
@@ -142,8 +181,51 @@
|
||||
</CommandBar>
|
||||
</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 -->
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
@@ -170,8 +252,7 @@
|
||||
<!-- Read-Only Event -->
|
||||
<Grid
|
||||
x:Name="ReadOnlyDetailsGrid"
|
||||
RowSpacing="6"
|
||||
Visibility="{x:Bind ReadOnlyToggle.IsChecked, Mode=OneWay}">
|
||||
RowSpacing="6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="16" />
|
||||
@@ -195,7 +276,7 @@
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
ColumnSpacing="6"
|
||||
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringEvent}">
|
||||
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringEvent, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -208,12 +289,6 @@
|
||||
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetRecurrenceString(ViewModel.CurrentEvent), Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Editable Event -->
|
||||
<Grid Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(ReadOnlyDetailsGrid.Visibility), Mode=OneWay}">
|
||||
<TextBlock Text="editing" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -238,7 +313,8 @@
|
||||
<AutoSuggestBox
|
||||
Margin="6,0"
|
||||
BorderThickness="0"
|
||||
PlaceholderText="Invite someone" />
|
||||
PlaceholderText="Invite someone"
|
||||
Visibility="{x:Bind ViewModel.IsCurrentUserOrganizer, Mode=OneWay}" />
|
||||
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
@@ -251,6 +327,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<PersonPicture
|
||||
@@ -285,6 +362,19 @@
|
||||
Text="{x:Bind domain:Translator.CalendarEventDetails_Organizer}" />
|
||||
</Border>
|
||||
</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>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
@@ -296,7 +386,8 @@
|
||||
<Grid
|
||||
x:Name="AttachmentsGrid"
|
||||
Grid.Column="2"
|
||||
Style="{StaticResource EventDetailsPanelGridStyle}">
|
||||
Style="{StaticResource EventDetailsPanelGridStyle}"
|
||||
Visibility="{x:Bind ViewModel.HasAttachments, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -304,6 +395,82 @@
|
||||
|
||||
<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>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Calendar.Views.Abstract;
|
||||
|
||||
namespace Wino.Calendar.Views;
|
||||
@@ -9,4 +10,28 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract
|
||||
{
|
||||
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 SQLite;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Calendar;
|
||||
@@ -15,7 +16,9 @@ public class AccountCalendar : IAccountCalendar
|
||||
public string SynchronizationDeltaToken { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
public bool IsSynchronizationEnabled { get; set; } = true;
|
||||
public bool IsExtended { get; set; } = true;
|
||||
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
|
||||
|
||||
/// <summary>
|
||||
/// 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 Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
@@ -9,9 +10,11 @@ public interface IAccountCalendar
|
||||
string TextColorHex { get; set; }
|
||||
string BackgroundColorHex { get; set; }
|
||||
bool IsPrimary { get; set; }
|
||||
bool IsSynchronizationEnabled { get; set; }
|
||||
Guid AccountId { get; set; }
|
||||
string RemoteCalendarId { get; set; }
|
||||
bool IsExtended { get; set; }
|
||||
CalendarItemShowAs DefaultShowAs { get; set; }
|
||||
Guid Id { 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.Enums;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
|
||||
namespace Wino.Core.ViewModels;
|
||||
@@ -9,14 +11,19 @@ public class CalendarBaseViewModel : CoreBaseViewModel,
|
||||
IRecipient<CalendarItemUpdated>,
|
||||
IRecipient<CalendarItemDeleted>
|
||||
{
|
||||
public void Receive(CalendarItemAdded message) => OnCalendarItemAdded(message.CalendarItem);
|
||||
public void Receive(CalendarItemUpdated message) => OnCalendarItemUpdated(message.CalendarItem);
|
||||
public void Receive(CalendarItemDeleted message) => OnCalendarItemDeleted(message.CalendarItem);
|
||||
public void Receive(CalendarItemAdded message) => DispatchToUIThread(() => OnCalendarItemAdded(message.CalendarItem));
|
||||
public void Receive(CalendarItemUpdated message) => DispatchToUIThread(() => OnCalendarItemUpdated(message.CalendarItem, message.Source));
|
||||
public void Receive(CalendarItemDeleted message) => DispatchToUIThread(() => OnCalendarItemDeleted(message.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) { }
|
||||
|
||||
private void DispatchToUIThread(Action action)
|
||||
{
|
||||
_ = ExecuteUIThread(action);
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
@@ -35,3 +42,4 @@ public class CalendarBaseViewModel : CoreBaseViewModel,
|
||||
Messenger.Unregister<CalendarItemDeleted>(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,18 @@ public class CoreBaseViewModel : ObservableRecipient, INavigationAware
|
||||
|
||||
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);
|
||||
|
||||
protected virtual void OnDispatcherAssigned() { }
|
||||
|
||||
@@ -141,6 +141,7 @@ public static class GoogleIntegratorExtensions
|
||||
Id = Guid.NewGuid(),
|
||||
TimeZone = calendarListEntry.TimeZone,
|
||||
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
||||
IsSynchronizationEnabled = true,
|
||||
};
|
||||
|
||||
// Bg color must present. Generate one if doesnt exists.
|
||||
|
||||
@@ -183,6 +183,7 @@ public static class OutlookIntegratorExtensions
|
||||
RemoteCalendarId = outlookCalendar.Id,
|
||||
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
|
||||
Name = outlookCalendar.Name,
|
||||
IsSynchronizationEnabled = true,
|
||||
IsExtended = true,
|
||||
};
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ public record AcceptEventRequest(CalendarItem Item, string ResponseMessage = nul
|
||||
Item.Status = CalendarItemStatus.Accepted;
|
||||
|
||||
// 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()
|
||||
{
|
||||
// If acceptance fails, revert to the previous status
|
||||
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;
|
||||
|
||||
// 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()
|
||||
{
|
||||
// If decline fails, revert to the previous status
|
||||
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;
|
||||
|
||||
// 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()
|
||||
{
|
||||
// If tentative acceptance fails, revert to the previous status
|
||||
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()
|
||||
{
|
||||
// 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()
|
||||
@@ -42,12 +42,12 @@ public record UpdateCalendarEventRequest(CalendarItem Item, List<CalendarEventAt
|
||||
if (OriginalItem != null && OriginalAttendees != null)
|
||||
{
|
||||
// Send the original item back to restore UI state
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(OriginalItem));
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemUpdated(OriginalItem, CalendarItemUpdateSource.ClientReverted));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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);
|
||||
|
||||
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.
|
||||
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 remotePrimaryCalendarId = GetPrimaryCalendarId(calendarListResponse.Items);
|
||||
|
||||
List<AccountCalendar> insertedCalendars = new();
|
||||
List<AccountCalendar> updatedCalendars = new();
|
||||
@@ -596,14 +599,21 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
{
|
||||
// Insert new calendar.
|
||||
var localCalendar = calendar.AsCalendar(Account.Id);
|
||||
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||
insertedCalendars.Add(localCalendar);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing calendar. Right now we only update the name.
|
||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar))
|
||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId))
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -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 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)
|
||||
|
||||
@@ -1961,7 +1961,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
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;
|
||||
|
||||
@@ -2078,6 +2080,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -2110,14 +2113,18 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
{
|
||||
// Insert new calendar.
|
||||
var localCalendar = calendar.AsCalendar(Account);
|
||||
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||
insertedCalendars.Add(localCalendar);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing calendar. Right now we only update the name.
|
||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar))
|
||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId))
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -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 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
|
||||
|
||||
@@ -26,6 +26,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IStatePersistanceService _statePersistanceService;
|
||||
private bool isLoaded = false;
|
||||
|
||||
public MailAccount Account { get; set; }
|
||||
@@ -60,12 +61,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
public AccountDetailsPageViewModel(IMailDialogService dialogService,
|
||||
IAccountService accountService,
|
||||
IFolderService folderService,
|
||||
ICalendarService calendarService)
|
||||
ICalendarService calendarService,
|
||||
IStatePersistanceService statePersistanceService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_accountService = accountService;
|
||||
_folderService = folderService;
|
||||
_calendarService = calendarService;
|
||||
_statePersistanceService = statePersistanceService;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -132,6 +135,8 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
|
||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||
|
||||
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1;
|
||||
|
||||
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
|
||||
|
||||
foreach (var folder in folderStructures)
|
||||
|
||||
@@ -225,6 +225,8 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
||||
existingCalendar.BackgroundColorHex = accountCalendar.BackgroundColorHex;
|
||||
existingCalendar.IsExtended = accountCalendar.IsExtended;
|
||||
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.IsExtended = accountCalendar.IsExtended;
|
||||
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)
|
||||
{
|
||||
// Set initial tab to Mail (index 1)
|
||||
TabSelector.SelectedItem = TabSelector.Items[1];
|
||||
var targetTabIndex = ViewModel.SelectedTabIndex;
|
||||
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.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
@@ -77,6 +78,12 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract,
|
||||
|
||||
private async Task RenderDescriptionAsync()
|
||||
{
|
||||
if (DispatcherQueue != null && !DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
await DispatcherQueue.EnqueueAsync(RenderDescriptionAsync);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewModel?.CurrentEvent?.CalendarItem == null)
|
||||
return;
|
||||
|
||||
@@ -141,6 +148,12 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract,
|
||||
|
||||
private async Task UpdateEditorThemeAsync()
|
||||
{
|
||||
if (DispatcherQueue != null && !DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
await DispatcherQueue.EnqueueAsync(UpdateEditorThemeAsync);
|
||||
return;
|
||||
}
|
||||
|
||||
await DOMLoadedTask.Task;
|
||||
|
||||
if (ViewModel.IsDarkWebviewRenderer)
|
||||
@@ -171,9 +184,9 @@ public sealed partial class EventDetailsPage : EventDetailsPageAbstract,
|
||||
_ = UpdateEditorThemeAsync();
|
||||
}
|
||||
|
||||
async void IRecipient<CalendarDescriptionRenderingRequested>.Receive(CalendarDescriptionRenderingRequested message)
|
||||
void IRecipient<CalendarDescriptionRenderingRequested>.Receive(CalendarDescriptionRenderingRequested message)
|
||||
{
|
||||
await RenderDescriptionAsync();
|
||||
_ = RenderDescriptionAsync();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
public record CalendarItemAdded(CalendarItem CalendarItem);
|
||||
public record CalendarItemUpdated(CalendarItem CalendarItem);
|
||||
public record CalendarItemUpdated(CalendarItem CalendarItem, CalendarItemUpdateSource Source);
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -64,4 +64,5 @@ public class DatabaseService : IDatabaseService
|
||||
Connection.CreateTableAsync<MailInvitationCalendarMapping>()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user