From 96cbbb0dc5d4b2d038a98c0dca3046e35dd2f951 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 24 Oct 2026 17:26:49 +0200 Subject: [PATCH] Make the per-calendar reminder toggle visible and keep what it sets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three faults made the existing toggle unreliable on iOS. Opening the filter or the drawer rebuilt the muted set from the five server-backed sources and replaced the whole thing, so every device-calendar mute was silently dropped — along with the mute of any calendar the server did not happen to return. Only the server-backed part is replaced now. Device-calendar events carry no reminders of their own, so the global default applied and Calendarr notified on top of the system Calendar app. They stay silent unless the event brings its own reminder; the per-calendar toggle can still switch them on, which it could not before because of the fault above. The toggle itself lived only behind a long press, with nothing to suggest it existed until after it had been used — the bell.slash icon appeared only once muted. It is a visible button in the row now, the way Android already does it, and the duplicate context-menu entry is gone. Local calendars use the new reminders endpoint, so a recipient's mute reaches the server instead of dying in a 404 that `try?` swallowed. Also adds the EventKit import that c811486 needed in five files: that commit left beta unable to build for either iOS or Mac Catalyst. Co-Authored-By: Claude Opus 5 --- .../UserInterfaceState.xcuserstate | Bin 11227 -> 11233 bytes ios/Calendarr iOS/Models/CalendarStore.swift | 2 + ios/Calendarr iOS/Services/CalendarrAPI.swift | 2 +- .../Services/NotificationScheduler.swift | 7 +- ios/Calendarr iOS/Views/AccountsView.swift | 1 + .../Views/Calendar/EventDetailSheet.swift | 1 + .../Views/Calendar/EventEditorSheet.swift | 1 + .../Views/CalendarFilterContent.swift | 72 +++++++++++------- 8 files changed, 55 insertions(+), 31 deletions(-) diff --git a/ios/Calendarr iOS.xcodeproj/project.xcworkspace/xcuserdata/scarriffle.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Calendarr iOS.xcodeproj/project.xcworkspace/xcuserdata/scarriffle.xcuserdatad/UserInterfaceState.xcuserstate index 2e585ff4748db7786bad6d88bf2fdefde07ad0b3..e2faf84c21435aac1934a3ba1c5fbd52f1d0054f 100644 GIT binary patch delta 981 zcmV~$eN0tV008g+QyHak?>YCKbI&>Ve7&2RZ5bJ*HAd|D5vVXEc)VCd!pfmMpARC2 zuPbB3%H|5t7(>PgF^!0NIWu7S(Z?!djEI>1(aOxNkdf7hi0JqGzUarI!G41UZcAUa zCwh2e>0L%~MYOu&V0l@cD61A}X`45s#V;FMmMssYTV-2Ik5y%ji?|Ir<%Y z5IdM-iSNM=;cUDE@4|UF9~a<49K<19g2On1qqqjw;yPT9W7xwDxDmJDv-kr31^;Io z_F6l~uCfo0ZO83P_Eq~PNhT@ePO_S$5|coZLkdU{DJD;n=SY;)k$Ms%9(jed zdgOg_j!cpt$TazzCemeeIbBIp=^A=3&7wK9kOpapw$T&xG##YFbd-L_64?rt#vWlA zER$uiToz&_>?!s%dxn*>FpID#t6{aQj@7ef*222k2kbL8#jdg&Y=+IUTkKD^z!uq( zli(yd9&$D~+nhXSxATNk-!%!*rLUMz@3u_P1ZYDwjNa+A!Gxw23OWk{CDJ+eYR zEBDGu8Iv!`R@o`LWVh^*z49ITu6$3%x~F+ zH^#24Y^?y57&1nPkBmq%Lt&{sTxGMU|I8RPzBaxwelUJF{xl|y zDP!80F=mZ9W5G-{(@etzCNi;UnYQVe!ptz&n^CjF90iGh10Q${l!GXUfof0#;-D71 z1onX@&34LD{zd2MqJ4wC#7?oLeVd(W=h{WK-wxOf_96S2-DUUM{r0!^JWF6J*u5-+`B)~) zV*yso9$}BN$5{yrvJeZi2&-U~tcumLI@ZeGW1q4ScA1T`aW=uOvp?80n_;tTjx9KG z!@0v*kth5jD0Yi|qCxBz2SvL$B94g@qEmE< zkK^J?F(`(_uow|n#h4fu6XLp<64PQv%!)agERnoju9lfHPZmkP49H^ngnUw#%I&gD zM&({vFPmkHY?W=YUA`^fk?+cGc}kv^=j4dIBCpB6<%0ZAC8$J|q*7IyG89lqVRfI{ zqV}lxe$}Q9t0U^TI-xq%1@(m*P=jhn4XZ2aXEmnA)r6W= 0 && ev.source != "device" let offsets = ev.reminders.isEmpty - ? (defaultMin >= 0 ? [defaultMin] : []) + ? (allowsDefault ? [defaultMin] : []) : ev.reminders for m in offsets { let fire = ev.startDate.addingTimeInterval(-Double(m) * 60) diff --git a/ios/Calendarr iOS/Views/AccountsView.swift b/ios/Calendarr iOS/Views/AccountsView.swift index e1b1c00..06a572d 100644 --- a/ios/Calendarr iOS/Views/AccountsView.swift +++ b/ios/Calendarr iOS/Views/AccountsView.swift @@ -1,5 +1,6 @@ import SwiftUI import UniformTypeIdentifiers +import EventKit struct AccountsView: View { let api: CalendarrAPI diff --git a/ios/Calendarr iOS/Views/Calendar/EventDetailSheet.swift b/ios/Calendarr iOS/Views/Calendar/EventDetailSheet.swift index faa52e8..1ea932a 100644 --- a/ios/Calendarr iOS/Views/Calendar/EventDetailSheet.swift +++ b/ios/Calendarr iOS/Views/Calendar/EventDetailSheet.swift @@ -1,4 +1,5 @@ import SwiftUI +import EventKit /// `.sheet(item:)` needs Identifiable and URL is not. A retroactive conformance /// on a stdlib type would be visible module-wide, so wrap it instead. diff --git a/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift b/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift index 01a11dc..dec5171 100644 --- a/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift +++ b/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift @@ -1,5 +1,6 @@ import SwiftUI import UniformTypeIdentifiers +import EventKit struct EventEditorSheet: View { let api: CalendarrAPI diff --git a/ios/Calendarr iOS/Views/CalendarFilterContent.swift b/ios/Calendarr iOS/Views/CalendarFilterContent.swift index e6df0c6..ec276a2 100644 --- a/ios/Calendarr iOS/Views/CalendarFilterContent.swift +++ b/ios/Calendarr iOS/Views/CalendarFilterContent.swift @@ -1,4 +1,5 @@ import SwiftUI +import EventKit /// A single calendar row in the flat, reorderable list. private struct CalRow: Identifiable { @@ -87,39 +88,44 @@ struct CalendarFilterContent: View { @ViewBuilder private func row(_ cal: CalRow) -> some View { let isVisible = !hidden.contains(cal.id) - Button { - if isVisible { hidden.insert(cal.id) } else { hidden.remove(cal.id) } - store.setCalendarHidden(cal.id, hidden: isVisible) - } label: { - HStack(spacing: 12) { - Circle() - .fill(Color(hex: cal.colorHex)) - .frame(width: 14, height: 14) - .opacity(isVisible ? 1.0 : 0.35) - Text(cal.name) - .foregroundStyle(isVisible ? .primary : .secondary) - .strikethrough(!isVisible, color: .secondary) - if cal.readOnly { - Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary) + let muted = reminderDisabled.contains(cal.id) + HStack(spacing: 8) { + Button { + if isVisible { hidden.insert(cal.id) } else { hidden.remove(cal.id) } + store.setCalendarHidden(cal.id, hidden: isVisible) + } label: { + HStack(spacing: 12) { + Circle() + .fill(Color(hex: cal.colorHex)) + .frame(width: 14, height: 14) + .opacity(isVisible ? 1.0 : 0.35) + Text(cal.name) + .foregroundStyle(isVisible ? .primary : .secondary) + .strikethrough(!isVisible, color: .secondary) + if cal.readOnly { + Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary) + } + Spacer() + Image(systemName: isVisible ? "eye" : "eye.slash") + .foregroundStyle(isVisible ? Color.accentColor : .secondary) } - Spacer() - if reminderDisabled.contains(cal.id) { - Image(systemName: "bell.slash").font(.caption).foregroundStyle(.secondary) - } - Image(systemName: isVisible ? "eye" : "eye.slash") - .foregroundStyle(isVisible ? Color.accentColor : .secondary) + .contentShape(Rectangle()) } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .contextMenu { + .buttonStyle(.plain) + + // A visible control, the way Android already does it. The mute used + // to live only behind a long press, with no hint it existed until + // after it had been switched on. Button { toggleReminders(forKey: cal.id) } label: { - let disabled = reminderDisabled.contains(cal.id) - Label(L10n.t(disabled ? "filter.reminders_on" : "filter.reminders_off", appLang), - systemImage: disabled ? "bell" : "bell.slash") + Image(systemName: muted ? "bell.slash" : "bell") + .foregroundStyle(muted ? .secondary : Color.accentColor) } + .buttonStyle(.borderless) + .accessibilityLabel(L10n.t(muted ? "filter.reminders_on" : "filter.reminders_off", appLang)) + } + .contextMenu { Button(role: .destructive) { hidden.remove(cal.id) banished.insert(cal.id) @@ -226,8 +232,16 @@ struct CalendarFilterContent: View { for sub in icalSubs { applyReminders("ical", sub.id, sub.remindersEnabled ?? true) } for acc in googleAccounts { for cal in acc.calendars ?? [] { applyReminders("google", cal.id, cal.remindersEnabled ?? true) } } for acc in haAccounts { for cal in acc.calendars ?? [] { applyReminders("homeassistant", cal.id, cal.remindersEnabled) } } - store.setReminderDisabledKeys(rd) - reminderDisabled = rd + // Only server-backed sources are reconciled here. Replacing the whole + // set would drop every device-calendar mute — and the mute of any + // calendar the server happened not to return on this call. + let serverSources: Set = ["local", "caldav", "ical", "google", "homeassistant"] + let keptLocally = store.reminderDisabledKeys.filter { key in + !serverSources.contains(key.split(separator: ":").first.map(String.init) ?? "") + } + let merged = rd.union(keptLocally) + store.setReminderDisabledKeys(merged) + reminderDisabled = merged // Build the flat row list (banished excluded), then sort by stored order. var combined: [CalRow] = []