From 5b6bd36ece986c4e7313ff5a20b3004aa0a4dbe6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 26 Jul 2026 18:49:40 +0200 Subject: [PATCH] fix(calendars): hold the mirror delete on the marker, not the stored ids The delete lock read the special-dates calendar ids out of preferences, which the sync only rewrites on its next pass. Restore the app's data (or lose the preferences any other way) while a mirror still exists and the feature is on, and the lock is off until then: the delete goes through, looks like it worked, and the next sync puts the calendar back. The calendar itself carries the durable CAL_SYNC2 marker the editor lock already trusts, so read the lock off that. It also covers the mirror of a type since switched off, which the same sync pass deletes on its own. Co-Authored-By: Claude Opus 5 (1M context) --- .../ui/calendars/CalendarsViewModel.kt | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt index 4d25bae..fb745ec 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt @@ -75,18 +75,24 @@ class CalendarsViewModel @Inject constructor( ) /** - * Managed special-dates calendars whose deletion would not stick. While its - * type is enabled, the sync recreates a missing mirror on the next pass - * (`SpecialDatesSyncEngine.reconcileCalendars`), so the delete would appear - * to work and then undo itself. Turning special dates off empties this set, - * and deleting a leftover mirror is a real delete from then on. + * Managed special-dates calendars whose deletion would not stick. While the + * feature is on, the sync owns every mirror: it recreates a missing one for + * an enabled type on the next pass and deletes the leftover of a disabled + * one (`SpecialDatesSyncEngine.reconcileCalendars`), so either way the + * delete would appear to work and then undo itself. Turning special dates + * off empties this set, and deleting a leftover mirror is a real delete from + * then on. + * + * Read off each calendar's own durable marker ([CalendarSource.isManaged], + * the `CAL_SYNC2` one the editor lock already trusts) rather than the stored + * ids, which are only rewritten on the next sync pass — a preferences loss + * would otherwise unlock a live mirror until then. */ val deleteLockedCalendarIds: StateFlow> = combine( + calendars, settingsPrefs.specialDatesEnabled, - settingsPrefs.specialDatesTypes, - settingsPrefs.specialDatesCalendars, - ) { enabled, types, byType -> - if (!enabled) emptySet() else types.mapNotNull { byType[it] }.toSet() + ) { sources, enabled -> + if (!enabled) emptySet() else sources.filter { it.isManaged }.map { it.id }.toSet() } .flowOn(io) .stateIn(