From ca4c87565938c448e381f8f112ed4797adee7bd5 Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Fri, 3 Jul 2026 22:05:08 +0200 Subject: [PATCH] fix(visibility): auto-reconcile server sidebar_hidden on launch/resume/sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiding or showing a calendar on the web sets enabled=false + sidebar_hidden server-side, so the server stops returning that calendar's events entirely. The app synced the filter toggle TO the server (setCalendarHiddenSynced) but never pulled the server's sidebar_hidden flags back, so a calendar hidden or re-enabled on the web (or another device) was never reflected until a manual sync or relaunch — the "hide it on the web and the app won't bring it back" bug. Add reconcileCalendarVisibility(): pulls the account lists and updates the local hidden set from the server's sidebar_hidden flags for external calendars (local/ical keys untouched). It runs before the initial load, on manual sync, and on a new ON_RESUME lifecycle hook — forcing a refetch when the set changed so a re-enabled calendar's events reappear on their own. Mirrors the iOS fix. Co-Authored-By: Claude Sonnet 4.6 --- .../calendarr/ui/calendar/CalendarScreen.kt | 11 ++++ .../ui/calendar/CalendarViewModel.kt | 52 +++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt index 8487fa3..ba2c4de 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt @@ -109,6 +109,17 @@ fun CalendarScreen( context, state.events, state.reminderDisabledKeys, vm.defaultReminderMinutes ) } + // Re-check server-side calendar visibility whenever the app returns to the + // foreground (e.g. after hiding/showing a calendar on the web or another + // device); reloads only if something changed. + val lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current + androidx.compose.runtime.DisposableEffect(lifecycleOwner) { + val observer = androidx.lifecycle.LifecycleEventObserver { _, event -> + if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) vm.onAppResumed() + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } var viewMenuOpen by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index 71a7486..cedac14 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -89,7 +89,7 @@ class CalendarViewModel @Inject constructor( init { loadWritableCalendars() loadGroups() - initialLoad() + initialLoad(reconcile = true) } /** @@ -97,12 +97,15 @@ class CalendarViewModel @Inject constructor( * mark ready. Entering the app fully-loaded avoids the first-open jank of * loading a big batch while the user is already scrolling. */ - private fun initialLoad() { + private fun initialLoad(reconcile: Boolean = false) { val months = settingsStore.cacheMonths.toLong() val today = LocalDate.now().withDayOfMonth(1) val start = instant(today.minusMonths(months)) val end = instant(today.plusMonths(months + 1)) viewModelScope.launch { + // Honour a calendar hidden/shown on the web BEFORE the first load so + // the visibility filter is correct before events are rendered. + if (reconcile) reconcileCalendarVisibility() loadRange(start, end, background = false) markReady() } @@ -365,7 +368,50 @@ class CalendarViewModel @Inject constructor( fun syncWithServer() { invalidateCache() loadGroups() - initialLoad() + initialLoad(reconcile = true) + } + + /** + * Called when the app returns to the foreground. Re-checks the server's + * per-calendar visibility (a calendar may have been hidden/shown on the web + * or another device meanwhile) and reloads only if something changed. + */ + fun onAppResumed() { + viewModelScope.launch { + if (reconcileCalendarVisibility()) { + invalidateCache() + loadGroups() + initialLoad(reconcile = false) // just reconciled above + } + } + } + + /** + * Reconcile the local hidden set with the server's per-calendar + * `sidebar_hidden` flags for external calendars (CalDAV / Google / HA). + * Returns `true` if the hidden set changed, so the caller can force a + * refetch — a calendar re-enabled on the web has NO events in the cache + * (the server excludes a hidden calendar's events entirely). Local / iCal + * hidden keys have no server flag and are left untouched. + */ + private suspend fun reconcileCalendarVisibility(): Boolean { + val caldav = runCatching { repository.getCalDAVAccounts() }.getOrDefault(emptyList()) + val google = runCatching { repository.getGoogleAccounts() }.getOrDefault(emptyList()) + val ha = runCatching { repository.getHomeAssistantAccounts() }.getOrDefault(emptyList()) + + val hidden = settingsStore.hiddenCalendarKeys.toMutableSet() + fun apply(source: String, id: Int, serverHidden: Boolean) { + val key = calendarKey(source, id.toString()) + if (serverHidden) hidden.add(key) else hidden.remove(key) + } + caldav.forEach { acc -> acc.calendars?.forEach { apply("caldav", it.id, it.sidebarHidden) } } + google.forEach { acc -> acc.calendars?.forEach { apply("google", it.id, it.sidebarHidden) } } + ha.forEach { acc -> acc.calendars?.forEach { apply("homeassistant", it.id, it.sidebarHidden) } } + + if (hidden == settingsStore.hiddenCalendarKeys) return false + settingsStore.hiddenCalendarKeys = hidden + _state.update { it.copy(hiddenKeys = hidden) } + return true } fun clearError() = _state.update { it.copy(error = null) }