From 9db04361cca7d02e737f3b613514e14cd6f0e685 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 4 Jul 2026 18:56:32 +0200 Subject: [PATCH] feat(sharing): show shared calendars read-only (parity with server/Android) - Parse the server's new read_only flag on events. - Filter sheet: a calendar shared with me is listed under the owner's name (sharedBy) with a lock icon when I only have read access. - loadWritableCalendars: exclude read-only shared calendars so they no longer appear in the event editor (a save would 403). The group-title prefix removal is server-side and needs no iOS change. Co-Authored-By: Claude Opus 4.8 --- Calendarr iOS/Models/CalEvent.swift | 5 ++++- Calendarr iOS/Models/CalendarStore.swift | 4 +++- Calendarr iOS/Views/CalendarFilterSheet.swift | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Calendarr iOS/Models/CalEvent.swift b/Calendarr iOS/Models/CalEvent.swift index fb820ea..e53f30a 100644 --- a/Calendarr iOS/Models/CalEvent.swift +++ b/Calendarr iOS/Models/CalEvent.swift @@ -65,6 +65,8 @@ struct CalEvent: Identifiable, Hashable { var displayTitle: String? = nil // Reminder offsets in minutes-before-start (0 = at start). Local events only. var reminders: [Int] = [] + // True for events from a calendar shared with the user read-only. + var readOnly: Bool = false // Group view supplies a server-resolved colour; otherwise per-event then calendar colour. var effectiveColor: String { displayColor ?? color ?? calendarColor } @@ -107,7 +109,8 @@ struct CalEvent: Identifiable, Hashable { isGroupEvent: json["is_group_event"] as? Bool ?? false, displayColor: (json["display_color"] as? String).flatMap { $0.isEmpty ? nil : $0 }, displayTitle: (json["display_title"] as? String).flatMap { $0.isEmpty ? nil : $0 }, - reminders: (json["reminders"] as? [Int]) ?? (json["reminders"] as? [Any])?.compactMap { ($0 as? Int) ?? Int("\($0)") } ?? [] + reminders: (json["reminders"] as? [Int]) ?? (json["reminders"] as? [Any])?.compactMap { ($0 as? Int) ?? Int("\($0)") } ?? [], + readOnly: json["read_only"] as? Bool ?? false ) } } diff --git a/Calendarr iOS/Models/CalendarStore.swift b/Calendarr iOS/Models/CalendarStore.swift index f13fd2c..6c63a61 100644 --- a/Calendarr iOS/Models/CalendarStore.swift +++ b/Calendarr iOS/Models/CalendarStore.swift @@ -597,7 +597,9 @@ class CalendarStore { async let haCals = (try? await api.getHACalendars()) ?? [] var result: [WritableCalendar] = [] - for cal in await localCals { + // Skip read-only shared calendars — offering them in the event editor + // only leads to a 403 on save. Own + read_write (incl. group) stay. + for cal in await localCals where cal.owned || cal.permission == "read_write" { result.append(WritableCalendar(id: "local-\(cal.id)", name: cal.name, color: cal.color, source: "local", numericId: cal.id)) } for acc in await caldavAccs where acc.enabled { diff --git a/Calendarr iOS/Views/CalendarFilterSheet.swift b/Calendarr iOS/Views/CalendarFilterSheet.swift index ccd79dc..63f8efb 100644 --- a/Calendarr iOS/Views/CalendarFilterSheet.swift +++ b/Calendarr iOS/Views/CalendarFilterSheet.swift @@ -45,8 +45,12 @@ struct CalendarFilterSheet: View { if !visibleLocals.isEmpty { Section(L10n.t("accounts.local.header", appLang)) { ForEach(visibleLocals) { cal in - row(name: cal.name, colorHex: cal.color, - key: CalendarStore.calendarKey(source: "local", calendarId: "\(cal.id)")) + // A calendar shared with me is shown under the owner's + // name and flagged read-only when I can't write to it. + row(name: cal.owned ? cal.name : (cal.sharedBy ?? cal.name), + colorHex: cal.color, + key: CalendarStore.calendarKey(source: "local", calendarId: "\(cal.id)"), + readOnly: !cal.owned && cal.permission != "read_write") } } } @@ -140,7 +144,7 @@ struct CalendarFilterSheet: View { } @ViewBuilder - private func row(name: String, colorHex: String, key: String) -> some View { + private func row(name: String, colorHex: String, key: String, readOnly: Bool = false) -> some View { let isVisible = !hidden.contains(key) Button { if isVisible { hidden.insert(key) } else { hidden.remove(key) } @@ -156,6 +160,11 @@ struct CalendarFilterSheet: View { Text(name) .foregroundStyle(isVisible ? .primary : .secondary) .strikethrough(!isVisible, color: .secondary) + if readOnly { + Image(systemName: "lock.fill") + .font(.caption2) + .foregroundStyle(.secondary) + } Spacer() if reminderDisabled.contains(key) { Image(systemName: "bell.slash")