Immidiate ui reflection for calendar events and some more error handling.

This commit is contained in:
Burak Kaan Köse
2026-04-07 16:48:46 +02:00
parent 3db54023a4
commit 71fc883e47
53 changed files with 1482 additions and 393 deletions
+274 -44
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
@@ -14,11 +15,13 @@ using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Services;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
using Wino.Messaging.UI;
@@ -119,7 +122,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
public partial bool IsCalendarEnabled { get; set; } = true;
[ObservableProperty]
public partial IReadOnlyList<CalendarItemViewModel> CalendarItems { get; set; } = [];
public partial ObservableCollection<CalendarItemViewModel> CalendarItems { get; set; } = new();
#endregion
@@ -153,7 +156,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
private bool _subscriptionsAttached;
private CancellationTokenSource _pageLifetimeCts = new();
private long _pageLifetimeVersion;
private List<CalendarItemViewModel> _loadedCalendarItems = [];
private Dictionary<Guid, CalendarItemViewModel> _loadedCalendarItems = new();
[ObservableProperty]
public partial CalendarSettings CurrentSettings { get; set; }
@@ -323,8 +326,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
CurrentVisibleRange = null;
VisibleDateRangeText = string.Empty;
LoadedDateWindow = null;
_loadedCalendarItems = [];
CalendarItems = [];
_loadedCalendarItems = new();
CalendarItems = new();
}
public void Dispose()
@@ -594,8 +597,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
{
if (loadedItems != null)
{
_loadedCalendarItems = loadedItems;
CalendarItems = loadedItems;
ReplaceLoadedCalendarItems(loadedItems);
}
EnsureSelectedQuickEventAccountCalendar();
@@ -656,8 +658,10 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
{
var loadedItems = new Dictionary<Guid, CalendarItemViewModel>();
var loadPeriod = new TimeRange(loadedDateWindow.StartDate, loadedDateWindow.EndDate);
var activeCalendars = AccountCalendarStateService.ActiveCalendars.ToList();
var pendingCalendarItemIds = await GetPendingCalendarItemIdsAsync(activeCalendars, lifetimeVersion).ConfigureAwait(false);
foreach (var calendarViewModel in AccountCalendarStateService.ActiveCalendars)
foreach (var calendarViewModel in activeCalendars)
{
if (!IsPageActive(lifetimeVersion))
return [];
@@ -672,12 +676,16 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
if (!loadedItems.ContainsKey(calendarItem.Id))
{
loadedItems.Add(calendarItem.Id, new CalendarItemViewModel(calendarItem));
loadedItems.Add(calendarItem.Id, CreateCalendarItemViewModel(calendarItem, pendingCalendarItemIds));
}
}
}
return loadedItems.Values.ToList();
return loadedItems.Values
.OrderBy(item => item.StartDate)
.ThenBy(item => item.EndDate)
.ThenBy(item => item.Id)
.ToList();
}
private static bool IsSameVisibleRange(VisibleDateRange current, VisibleDateRange next)
@@ -754,41 +762,12 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
await ReloadCurrentVisibleRangeAsync().ConfigureAwait(false);
}
protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
protected override void OnCalendarItemDeleted(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemDeleted(calendarItem);
base.OnCalendarItemDeleted(calendarItem, source);
if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id ||
DisplayDetailsCalendarItemViewModel?.CalendarItem?.RecurringCalendarItemId == calendarItem.Id)
{
DisplayDetailsCalendarItemViewModel = null;
}
if (ShouldReloadFor(calendarItem))
{
_ = ReloadCurrentVisibleRangeAsync();
}
}
protected override void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
{
base.OnCalendarItemUpdated(calendarItem, source);
if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id)
{
calendarItem.AssignedCalendar ??= DisplayDetailsCalendarItemViewModel.AssignedCalendar;
DisplayDetailsCalendarItemViewModel = new CalendarItemViewModel(calendarItem);
}
if (ShouldReloadFor(calendarItem))
{
_ = ReloadCurrentVisibleRangeAsync();
}
}
protected override void OnCalendarItemAdded(CalendarItem calendarItem)
{
base.OnCalendarItemAdded(calendarItem);
if (calendarItem == null)
return;
if (calendarItem.IsRecurringParent)
{
@@ -796,21 +775,272 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
return;
}
if (ShouldReloadFor(calendarItem))
var existingItemId = FindLoadedCalendarItemId(calendarItem);
if (!existingItemId.HasValue)
return;
RemoveLoadedCalendarItem(existingItemId.Value, calendarItem);
}
protected override void OnCalendarItemUpdated(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemUpdated(calendarItem, source);
ApplyCalendarItemUpsert(calendarItem, source);
}
protected override void OnCalendarItemAdded(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemAdded(calendarItem, source);
ApplyCalendarItemUpsert(calendarItem, source);
}
private async Task<HashSet<Guid>> GetPendingCalendarItemIdsAsync(IEnumerable<AccountCalendarViewModel> activeCalendars, long lifetimeVersion)
{
var pendingCalendarItemIds = new HashSet<Guid>();
var accountIds = activeCalendars
.Select(calendar => calendar.Account.Id)
.Where(accountId => accountId != Guid.Empty)
.Distinct()
.ToList();
foreach (var accountId in accountIds)
{
if (!IsPageActive(lifetimeVersion))
return pendingCalendarItemIds;
IWinoSynchronizerBase synchronizer;
try
{
synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
return pendingCalendarItemIds;
}
if (synchronizer == null)
continue;
foreach (var pendingCalendarItemId in synchronizer.GetPendingCalendarOperationIds())
{
pendingCalendarItemIds.Add(pendingCalendarItemId);
}
}
return pendingCalendarItemIds;
}
private void ApplyCalendarItemUpsert(CalendarItem calendarItem, EntityUpdateSource source)
{
if (calendarItem == null)
return;
if (calendarItem.IsRecurringParent)
{
_ = ReloadCurrentVisibleRangeAsync();
return;
}
var existingItemId = FindLoadedCalendarItemId(calendarItem);
var shouldDisplay = ShouldDisplayCalendarItem(calendarItem);
if (!shouldDisplay)
{
if (existingItemId.HasValue)
{
RemoveLoadedCalendarItem(existingItemId.Value, calendarItem);
}
return;
}
var newViewModel = CreateCalendarItemViewModel(calendarItem, source);
if (existingItemId.HasValue)
{
ReplaceLoadedCalendarItem(existingItemId.Value, newViewModel);
}
else
{
InsertLoadedCalendarItem(newViewModel);
}
}
private bool ShouldReloadFor(CalendarItem calendarItem)
private CalendarItemViewModel CreateCalendarItemViewModel(CalendarItem calendarItem, EntityUpdateSource source)
=> CreateCalendarItemViewModel(
calendarItem,
source == EntityUpdateSource.ClientUpdated ? new HashSet<Guid> { calendarItem.Id } : null,
source);
private CalendarItemViewModel CreateCalendarItemViewModel(CalendarItem calendarItem, ISet<Guid> pendingCalendarItemIds, EntityUpdateSource source = EntityUpdateSource.Server)
{
calendarItem.AssignedCalendar ??= ResolveAssignedCalendar(calendarItem.CalendarId);
return new CalendarItemViewModel(calendarItem)
{
IsBusy = source == EntityUpdateSource.ClientUpdated || HasPendingCalendarOperation(calendarItem, pendingCalendarItemIds)
};
}
private void ReplaceLoadedCalendarItems(IEnumerable<CalendarItemViewModel> loadedItems)
{
var loadedItemsList = loadedItems?.ToList() ?? [];
CalendarItems = new ObservableCollection<CalendarItemViewModel>(loadedItemsList);
_loadedCalendarItems = loadedItemsList.ToDictionary(item => item.Id);
}
private void InsertLoadedCalendarItem(CalendarItemViewModel calendarItemViewModel)
{
var insertionIndex = 0;
while (insertionIndex < CalendarItems.Count && CompareCalendarItems(CalendarItems[insertionIndex], calendarItemViewModel) <= 0)
{
insertionIndex++;
}
CalendarItems.Insert(insertionIndex, calendarItemViewModel);
_loadedCalendarItems[calendarItemViewModel.Id] = calendarItemViewModel;
if (IsDisplayDetailsMatch(calendarItemViewModel.CalendarItem))
{
DisplayDetailsCalendarItemViewModel = calendarItemViewModel;
}
}
private void ReplaceLoadedCalendarItem(Guid existingItemId, CalendarItemViewModel replacementViewModel)
{
if (!_loadedCalendarItems.TryGetValue(existingItemId, out var existingViewModel))
{
InsertLoadedCalendarItem(replacementViewModel);
return;
}
replacementViewModel.IsSelected = existingViewModel.IsSelected;
var existingIndex = CalendarItems.IndexOf(existingViewModel);
if (existingIndex >= 0)
{
CalendarItems[existingIndex] = replacementViewModel;
}
_loadedCalendarItems.Remove(existingItemId);
_loadedCalendarItems[replacementViewModel.Id] = replacementViewModel;
if (existingIndex >= 0)
{
MoveCalendarItemToSortedPosition(replacementViewModel, existingIndex);
}
if (IsDisplayDetailsMatch(replacementViewModel.CalendarItem, existingItemId))
{
DisplayDetailsCalendarItemViewModel = replacementViewModel;
}
}
private void RemoveLoadedCalendarItem(Guid existingItemId, CalendarItem calendarItem)
{
if (_loadedCalendarItems.TryGetValue(existingItemId, out var existingViewModel))
{
CalendarItems.Remove(existingViewModel);
_loadedCalendarItems.Remove(existingItemId);
}
if (IsDisplayDetailsMatch(calendarItem, existingItemId))
{
DisplayDetailsCalendarItemViewModel = null;
}
}
private void MoveCalendarItemToSortedPosition(CalendarItemViewModel calendarItemViewModel, int previousIndex)
{
if (previousIndex < 0)
return;
var targetIndex = 0;
while (targetIndex < CalendarItems.Count && CompareCalendarItems(CalendarItems[targetIndex], calendarItemViewModel) <= 0)
{
targetIndex++;
}
if (targetIndex > previousIndex)
{
targetIndex--;
}
if (targetIndex != previousIndex)
{
CalendarItems.Move(previousIndex, targetIndex);
}
}
private Guid? FindLoadedCalendarItemId(CalendarItem calendarItem)
{
if (calendarItem == null)
return null;
if (_loadedCalendarItems.ContainsKey(calendarItem.Id))
return calendarItem.Id;
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
if (trackedLocalItemId.HasValue && _loadedCalendarItems.ContainsKey(trackedLocalItemId.Value))
return trackedLocalItemId.Value;
return null;
}
private bool ShouldDisplayCalendarItem(CalendarItem calendarItem)
{
if (calendarItem == null || LoadedDateWindow == null)
return false;
if (calendarItem.IsHidden || calendarItem.IsRecurringParent || !IsCalendarActive(calendarItem.CalendarId))
return false;
var loadedWindow = new TimeRange(LoadedDateWindow.StartDate, LoadedDateWindow.EndDate);
return loadedWindow.OverlapsWith(calendarItem.Period);
}
private bool IsDisplayDetailsMatch(CalendarItem calendarItem, Guid? existingItemId = null)
{
if (DisplayDetailsCalendarItemViewModel == null || calendarItem == null)
return false;
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
return DisplayDetailsCalendarItemViewModel.Id == calendarItem.Id ||
(existingItemId.HasValue && DisplayDetailsCalendarItemViewModel.Id == existingItemId.Value) ||
(trackedLocalItemId.HasValue && DisplayDetailsCalendarItemViewModel.Id == trackedLocalItemId.Value) ||
DisplayDetailsCalendarItemViewModel.CalendarItem?.RecurringCalendarItemId == calendarItem.Id;
}
private bool HasPendingCalendarOperation(CalendarItem calendarItem, ISet<Guid> pendingCalendarItemIds)
{
if (calendarItem == null || pendingCalendarItemIds == null || pendingCalendarItemIds.Count == 0)
return false;
if (pendingCalendarItemIds.Contains(calendarItem.Id))
return true;
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
return trackedLocalItemId.HasValue && pendingCalendarItemIds.Contains(trackedLocalItemId.Value);
}
private AccountCalendarViewModel ResolveAssignedCalendar(Guid calendarId)
=> AccountCalendarStateService.AllCalendars.FirstOrDefault(calendar => calendar.Id == calendarId);
private static int CompareCalendarItems(CalendarItemViewModel left, CalendarItemViewModel right)
{
var compareResult = DateTime.Compare(left?.StartDate ?? DateTime.MinValue, right?.StartDate ?? DateTime.MinValue);
if (compareResult != 0)
return compareResult;
compareResult = DateTime.Compare(left?.EndDate ?? DateTime.MinValue, right?.EndDate ?? DateTime.MinValue);
if (compareResult != 0)
return compareResult;
return Nullable.Compare(left?.Id, right?.Id);
}
partial void OnIsAllDayChanged(bool value)
{
if (value)
@@ -12,6 +12,7 @@ using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
@@ -187,20 +188,20 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
await LoadCalendarItemTargetAsync(args);
}
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemUpdated(calendarItem, source);
// If the current event was updated, reload it
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
if (IsCurrentEventMatch(calendarItem))
{
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
if (source == CalendarItemUpdateSource.ClientUpdated || source == CalendarItemUpdateSource.ClientReverted)
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
{
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
CurrentEvent = new CalendarItemViewModel(calendarItem)
{
IsBusy = source == CalendarItemUpdateSource.ClientUpdated
IsBusy = source == EntityUpdateSource.ClientUpdated
};
foreach (var attendee in previousAttendees)
@@ -221,17 +222,54 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
}
}
protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
protected override async void OnCalendarItemAdded(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemDeleted(calendarItem);
base.OnCalendarItemAdded(calendarItem, source);
if (!IsCurrentEventMatch(calendarItem))
return;
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
{
CurrentEvent = new CalendarItemViewModel(calendarItem)
{
IsBusy = source == EntityUpdateSource.ClientUpdated
};
return;
}
var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
if (refreshedEvent != null)
{
CurrentEvent = new CalendarItemViewModel(refreshedEvent);
await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent);
}
}
protected override void OnCalendarItemDeleted(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemDeleted(calendarItem, source);
// If the current event was deleted, navigate back
if (CurrentEvent?.CalendarItem?.Id == calendarItem.Id || CurrentEvent?.CalendarItem.RecurringCalendarItemId == calendarItem.Id)
if (IsCurrentEventMatch(calendarItem))
{
NavigateBackToCalendar(forceReload: true);
}
}
private bool IsCurrentEventMatch(CalendarItem calendarItem)
{
if (CurrentEvent?.CalendarItem == null || calendarItem == null)
return false;
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
return CurrentEvent.CalendarItem.Id == calendarItem.Id ||
(trackedLocalItemId.HasValue && CurrentEvent.CalendarItem.Id == trackedLocalItemId.Value) ||
CurrentEvent.CalendarItem.RecurringCalendarItemId == calendarItem.Id;
}
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
{
try