Bring a muted calendar down to Android, and let it build again

Reminders only ever travelled app -> server here: reconcileCalendarVisibility
read sidebar_hidden and nothing else, and reminderDisabledCalendarKeys came
purely from SharedPreferences, so a calendar muted on the web or on iOS stayed
loud on Android. It now reconciles reminders_enabled for all five sources,
which also means fetching local calendars and iCal subscriptions — a failed
fetch yields an empty list and reconciles nothing, so it can never clear a mute
by accident.

Local calendars go through the new reminders endpoint, so a share recipient's
mute is stored per user instead of being refused.

EventEditorSheet could not compile: a KDoc comment spelled out the catch-all
MIME wildcard, and the slash inside it closed the comment two lines early, so
the remainder parsed as code. Rewritten as line comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-24 17:30:39 +02:00
co-authored by Claude Opus 5
parent 96cbbb0dc5
commit 2bdf134d9c
4 changed files with 34 additions and 6 deletions
@@ -357,7 +357,7 @@ class CalendarRepository @Inject constructor(
val body = jsonBody("reminders_enabled" to enabled)
when (source) {
"caldav" -> api.updateCalDAVCalendar(calendarId, body).ensureSuccess()
"local" -> api.updateLocalCalendar(calendarId, body).ensureSuccess()
"local" -> api.setLocalCalendarReminders(calendarId, body).ensureSuccess()
"ical" -> api.updateICalSubscription(calendarId, body).ensureSuccess()
"google" -> api.updateGoogleCalendar(calendarId, body).ensureSuccess()
"homeassistant" -> api.updateHACalendar(calendarId, body).ensureSuccess()
@@ -192,6 +192,11 @@ interface CalendarrApi {
@PUT("api/local/calendars/{id}/color")
suspend fun setLocalCalendarColor(@Path("id") id: Int, @Body body: RequestBody): Response<ResponseBody>
// Reminder-only update that works for owners AND share recipients
// (recipients get their own per-user mute; owners flip the calendar's flag).
@PUT("api/local/calendars/{id}/reminders")
suspend fun setLocalCalendarReminders(@Path("id") id: Int, @Body body: RequestBody): Response<ResponseBody>
@DELETE("api/local/calendars/{id}")
suspend fun deleteLocalCalendar(@Path("id") id: Int): Response<ResponseBody>
@@ -490,6 +490,11 @@ class CalendarViewModel @Inject constructor(
val caldav = runCatching { repository.getCalDAVAccounts() }.getOrDefault(emptyList())
val google = runCatching { repository.getGoogleAccounts() }.getOrDefault(emptyList())
val ha = runCatching { repository.getHomeAssistantAccounts() }.getOrDefault(emptyList())
// Needed for the reminder flag, which exists on all five sources. A
// failed fetch yields an empty list, which simply reconciles nothing —
// never a wrongly cleared mute.
val local = runCatching { repository.getLocalCalendars() }.getOrDefault(emptyList())
val ical = runCatching { repository.getICalSubscriptions() }.getOrDefault(emptyList())
val banished = settingsStore.banishedCalendarKeys.toMutableSet()
fun apply(source: String, id: Int, serverHidden: Boolean) {
@@ -500,9 +505,25 @@ class CalendarViewModel @Inject constructor(
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 (banished == settingsStore.banishedCalendarKeys) return false
settingsStore.banishedCalendarKeys = banished
_state.update { it.copy(banishedKeys = banished) }
// Reminders were only ever pushed app -> server, so a mute set on the
// web or on iOS never arrived here.
val muted = settingsStore.reminderDisabledCalendarKeys.toMutableSet()
fun applyReminders(source: String, id: Int, enabled: Boolean) {
val key = calendarKey(source, id.toString())
if (enabled) muted.remove(key) else muted.add(key)
}
local.forEach { applyReminders("local", it.id, it.remindersEnabled) }
ical.forEach { applyReminders("ical", it.id, it.remindersEnabled) }
caldav.forEach { acc -> acc.calendars?.forEach { applyReminders("caldav", it.id, it.remindersEnabled) } }
google.forEach { acc -> acc.calendars?.forEach { applyReminders("google", it.id, it.remindersEnabled) } }
ha.forEach { acc -> acc.calendars?.forEach { applyReminders("homeassistant", it.id, it.remindersEnabled) } }
val banishedChanged = banished != settingsStore.banishedCalendarKeys
val mutedChanged = muted != settingsStore.reminderDisabledCalendarKeys
if (!banishedChanged && !mutedChanged) return false
if (banishedChanged) settingsStore.banishedCalendarKeys = banished
if (mutedChanged) settingsStore.reminderDisabledCalendarKeys = muted
_state.update { it.copy(banishedKeys = banished, reminderDisabledKeys = muted) }
return true
}
@@ -473,8 +473,10 @@ private fun ReminderRow(
}
}
/** Mirrors the server allowlist. "*/*" is deliberately absent — the picker
* should not offer files the upload would reject. */
// Mirrors the server allowlist. The catch-all wildcard type is deliberately
// absent — the picker should not offer files the upload would reject.
// (Written as line comments: spelling that wildcard inside a block comment
// closes it early, which is what used to break this file.)
private val ATTACHMENT_MIME_TYPES = arrayOf(
"application/pdf",
"image/png", "image/jpeg", "image/webp", "image/gif",