Let a hidden iCal calendar become visible again
An iCal subscription could be banished but never un-banished. The server has always sent and accepted sidebar_hidden for subscriptions, but iOS left the field off ICalSubscription entirely and omitted "ical" from serverManagedSources, from setCalendarSidebarHidden and from both places that reconcile the server's flag. So the key went into banishedCalendarKeys and nothing ever took it out: refreshFromCache filters on that set as well as on hiddenCalendarKeys, which is why showing the calendar again left its events invisible and no amount of syncing helped. Closed on all four sides. The account models are also no longer brittle about absent fields. HomeAssistantAccount required auth_method, and CalDAVCalendar and GoogleCalendar required sidebar_hidden, with no custom decoder — one missing key threw, the `try?` at the call site turned that into an empty list, and the reconciliation silently did not run, stranding calendars as banished in exactly the same way. HACalendar already carried a lenient decoder for this reason; the other three now do too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab0c64a9e1
commit
9f864617c2
@@ -111,6 +111,16 @@ struct CalDAVCalendar: Codable, Identifiable {
|
|||||||
case sidebarHidden = "sidebar_hidden"
|
case sidebarHidden = "sidebar_hidden"
|
||||||
case remindersEnabled = "reminders_enabled"
|
case remindersEnabled = "reminders_enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(Int.self, forKey: .id)
|
||||||
|
name = try c.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||||
|
color = try c.decodeIfPresent(String.self, forKey: .color)
|
||||||
|
enabled = try c.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||||
|
sidebarHidden = try c.decodeIfPresent(Bool.self, forKey: .sidebarHidden) ?? false
|
||||||
|
remindersEnabled = try c.decodeIfPresent(Bool.self, forKey: .remindersEnabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocalCalendar: Codable, Identifiable {
|
struct LocalCalendar: Codable, Identifiable {
|
||||||
@@ -161,12 +171,27 @@ struct ICalSubscription: Codable, Identifiable {
|
|||||||
var refreshMinutes: Int
|
var refreshMinutes: Int
|
||||||
var lastFetched: String?
|
var lastFetched: String?
|
||||||
var remindersEnabled: Bool?
|
var remindersEnabled: Bool?
|
||||||
|
var sidebarHidden: Bool = false
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, name, url, color, enabled
|
case id, name, url, color, enabled
|
||||||
case refreshMinutes = "refresh_minutes"
|
case refreshMinutes = "refresh_minutes"
|
||||||
case lastFetched = "last_fetched"
|
case lastFetched = "last_fetched"
|
||||||
case remindersEnabled = "reminders_enabled"
|
case remindersEnabled = "reminders_enabled"
|
||||||
|
case sidebarHidden = "sidebar_hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(Int.self, forKey: .id)
|
||||||
|
name = try c.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||||
|
url = try c.decodeIfPresent(String.self, forKey: .url) ?? ""
|
||||||
|
color = try c.decodeIfPresent(String.self, forKey: .color) ?? "#4285f4"
|
||||||
|
enabled = try c.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||||
|
refreshMinutes = try c.decodeIfPresent(Int.self, forKey: .refreshMinutes) ?? 60
|
||||||
|
lastFetched = try c.decodeIfPresent(String.self, forKey: .lastFetched)
|
||||||
|
remindersEnabled = try c.decodeIfPresent(Bool.self, forKey: .remindersEnabled)
|
||||||
|
sidebarHidden = try c.decodeIfPresent(Bool.self, forKey: .sidebarHidden) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +214,16 @@ struct GoogleCalendar: Codable, Identifiable {
|
|||||||
case sidebarHidden = "sidebar_hidden"
|
case sidebarHidden = "sidebar_hidden"
|
||||||
case remindersEnabled = "reminders_enabled"
|
case remindersEnabled = "reminders_enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(Int.self, forKey: .id)
|
||||||
|
name = try c.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||||
|
color = try c.decodeIfPresent(String.self, forKey: .color)
|
||||||
|
enabled = try c.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||||
|
sidebarHidden = try c.decodeIfPresent(Bool.self, forKey: .sidebarHidden) ?? false
|
||||||
|
remindersEnabled = try c.decodeIfPresent(Bool.self, forKey: .remindersEnabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomeAssistantAccount: Codable, Identifiable {
|
struct HomeAssistantAccount: Codable, Identifiable {
|
||||||
@@ -202,6 +237,15 @@ struct HomeAssistantAccount: Codable, Identifiable {
|
|||||||
case id, name, url, calendars
|
case id, name, url, calendars
|
||||||
case authMethod = "auth_method"
|
case authMethod = "auth_method"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(Int.self, forKey: .id)
|
||||||
|
name = try c.decodeIfPresent(String.self, forKey: .name) ?? "Home Assistant"
|
||||||
|
url = try c.decodeIfPresent(String.self, forKey: .url) ?? ""
|
||||||
|
authMethod = try c.decodeIfPresent(String.self, forKey: .authMethod) ?? "token"
|
||||||
|
calendars = try c.decodeIfPresent([HACalendar].self, forKey: .calendars)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HACalendar: Codable, Identifiable {
|
struct HACalendar: Codable, Identifiable {
|
||||||
|
|||||||
@@ -287,7 +287,8 @@ class CalendarStore {
|
|||||||
async let c = (try? await api.getCalDAVAccounts()) ?? []
|
async let c = (try? await api.getCalDAVAccounts()) ?? []
|
||||||
async let g = (try? await api.getGoogleAccounts()) ?? []
|
async let g = (try? await api.getGoogleAccounts()) ?? []
|
||||||
async let h = (try? await api.getHomeAssistantAccounts()) ?? []
|
async let h = (try? await api.getHomeAssistantAccounts()) ?? []
|
||||||
let (caldav, google, ha) = await (c, g, h)
|
async let i = (try? await api.getICalSubscriptions()) ?? []
|
||||||
|
let (caldav, google, ha, ical) = await (c, g, h, i)
|
||||||
|
|
||||||
var b = banishedCalendarKeys
|
var b = banishedCalendarKeys
|
||||||
func applyServerHidden(_ source: String, _ id: Int, _ hidden: Bool) {
|
func applyServerHidden(_ source: String, _ id: Int, _ hidden: Bool) {
|
||||||
@@ -297,6 +298,7 @@ class CalendarStore {
|
|||||||
for acc in caldav { for cal in acc.calendars ?? [] { applyServerHidden("caldav", cal.id, cal.sidebarHidden) } }
|
for acc in caldav { for cal in acc.calendars ?? [] { applyServerHidden("caldav", cal.id, cal.sidebarHidden) } }
|
||||||
for acc in google { for cal in acc.calendars ?? [] { applyServerHidden("google", cal.id, cal.sidebarHidden) } }
|
for acc in google { for cal in acc.calendars ?? [] { applyServerHidden("google", cal.id, cal.sidebarHidden) } }
|
||||||
for acc in ha { for cal in acc.calendars ?? [] { applyServerHidden("homeassistant", cal.id, cal.sidebarHidden) } }
|
for acc in ha { for cal in acc.calendars ?? [] { applyServerHidden("homeassistant", cal.id, cal.sidebarHidden) } }
|
||||||
|
for sub in ical { applyServerHidden("ical", sub.id, sub.sidebarHidden) }
|
||||||
|
|
||||||
guard b != banishedCalendarKeys else { return false }
|
guard b != banishedCalendarKeys else { return false }
|
||||||
banishedCalendarKeys = b
|
banishedCalendarKeys = b
|
||||||
@@ -350,7 +352,7 @@ class CalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sources whose visibility is backed by the server's `sidebar_hidden`.
|
/// Sources whose visibility is backed by the server's `sidebar_hidden`.
|
||||||
static let serverManagedSources: Set<String> = ["caldav", "google", "homeassistant"]
|
static let serverManagedSources: Set<String> = ["caldav", "google", "homeassistant", "ical"]
|
||||||
|
|
||||||
var userCalendar: Calendar {
|
var userCalendar: Calendar {
|
||||||
var cal = Calendar.current
|
var cal = Calendar.current
|
||||||
|
|||||||
@@ -517,6 +517,7 @@ class CalendarrAPI {
|
|||||||
case "caldav": path = "/api/caldav/calendars/\(calendarId)"
|
case "caldav": path = "/api/caldav/calendars/\(calendarId)"
|
||||||
case "google": path = "/api/google/calendars/\(calendarId)"
|
case "google": path = "/api/google/calendars/\(calendarId)"
|
||||||
case "homeassistant": path = "/api/homeassistant/calendars/\(calendarId)"
|
case "homeassistant": path = "/api/homeassistant/calendars/\(calendarId)"
|
||||||
|
case "ical": path = "/api/ical/subscriptions/\(calendarId)"
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
_ = try await request(path, method: "PUT",
|
_ = try await request(path, method: "PUT",
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ struct CalendarFilterContent: View {
|
|||||||
for acc in caldavAccounts { for cal in acc.calendars ?? [] { applyServerHidden("caldav", cal.id, cal.sidebarHidden) } }
|
for acc in caldavAccounts { for cal in acc.calendars ?? [] { applyServerHidden("caldav", cal.id, cal.sidebarHidden) } }
|
||||||
for acc in googleAccounts { for cal in acc.calendars ?? [] { applyServerHidden("google", cal.id, cal.sidebarHidden) } }
|
for acc in googleAccounts { for cal in acc.calendars ?? [] { applyServerHidden("google", cal.id, cal.sidebarHidden) } }
|
||||||
for acc in haAccounts { for cal in acc.calendars ?? [] { applyServerHidden("homeassistant", cal.id, cal.sidebarHidden) } }
|
for acc in haAccounts { for cal in acc.calendars ?? [] { applyServerHidden("homeassistant", cal.id, cal.sidebarHidden) } }
|
||||||
|
for sub in icalSubs { applyServerHidden("ical", sub.id, sub.sidebarHidden) }
|
||||||
store.setBanishedCalendars(b)
|
store.setBanishedCalendars(b)
|
||||||
banished = b
|
banished = b
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user