-
Notifications
You must be signed in to change notification settings - Fork 57
feat: add sync metadata and improved all-day reminder support #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -104,17 +104,7 @@ public actor RemindersStore { | |||||||||||||||
| reminder.dueDateComponents = calendarComponents(from: dueDate) | ||||||||||||||||
| } | ||||||||||||||||
| try eventStore.save(reminder, commit: true) | ||||||||||||||||
| return ReminderItem( | ||||||||||||||||
| id: reminder.calendarItemIdentifier, | ||||||||||||||||
| title: reminder.title ?? "", | ||||||||||||||||
| notes: reminder.notes, | ||||||||||||||||
| isCompleted: reminder.isCompleted, | ||||||||||||||||
| completionDate: reminder.completionDate, | ||||||||||||||||
| priority: ReminderPriority(eventKitValue: Int(reminder.priority)), | ||||||||||||||||
| dueDate: date(from: reminder.dueDateComponents), | ||||||||||||||||
| listID: reminder.calendar.calendarIdentifier, | ||||||||||||||||
| listName: reminder.calendar.title | ||||||||||||||||
| ) | ||||||||||||||||
| return item(from: reminder) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| public func updateReminder(id: String, update: ReminderUpdate) async throws -> ReminderItem { | ||||||||||||||||
|
|
@@ -145,17 +135,7 @@ public actor RemindersStore { | |||||||||||||||
|
|
||||||||||||||||
| try eventStore.save(reminder, commit: true) | ||||||||||||||||
|
|
||||||||||||||||
| return ReminderItem( | ||||||||||||||||
| id: reminder.calendarItemIdentifier, | ||||||||||||||||
| title: reminder.title ?? "", | ||||||||||||||||
| notes: reminder.notes, | ||||||||||||||||
| isCompleted: reminder.isCompleted, | ||||||||||||||||
| completionDate: reminder.completionDate, | ||||||||||||||||
| priority: ReminderPriority(eventKitValue: Int(reminder.priority)), | ||||||||||||||||
| dueDate: date(from: reminder.dueDateComponents), | ||||||||||||||||
| listID: reminder.calendar.calendarIdentifier, | ||||||||||||||||
| listName: reminder.calendar.title | ||||||||||||||||
| ) | ||||||||||||||||
| return item(from: reminder) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| public func completeReminders(ids: [String]) async throws -> [ReminderItem] { | ||||||||||||||||
|
|
@@ -164,19 +144,7 @@ public actor RemindersStore { | |||||||||||||||
| let reminder = try reminder(withID: id) | ||||||||||||||||
| reminder.isCompleted = true | ||||||||||||||||
| try eventStore.save(reminder, commit: true) | ||||||||||||||||
| updated.append( | ||||||||||||||||
| ReminderItem( | ||||||||||||||||
| id: reminder.calendarItemIdentifier, | ||||||||||||||||
| title: reminder.title ?? "", | ||||||||||||||||
| notes: reminder.notes, | ||||||||||||||||
| isCompleted: reminder.isCompleted, | ||||||||||||||||
| completionDate: reminder.completionDate, | ||||||||||||||||
| priority: ReminderPriority(eventKitValue: Int(reminder.priority)), | ||||||||||||||||
| dueDate: date(from: reminder.dueDateComponents), | ||||||||||||||||
| listID: reminder.calendar.calendarIdentifier, | ||||||||||||||||
| listName: reminder.calendar.title | ||||||||||||||||
| ) | ||||||||||||||||
| ) | ||||||||||||||||
| updated.append(item(from: reminder)) | ||||||||||||||||
| } | ||||||||||||||||
| return updated | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -212,24 +180,31 @@ public actor RemindersStore { | |||||||||||||||
| let completionDate: Date? | ||||||||||||||||
| let priority: Int | ||||||||||||||||
| let dueDateComponents: DateComponents? | ||||||||||||||||
| let isAllDay: Bool | ||||||||||||||||
| let listID: String | ||||||||||||||||
| let listName: String | ||||||||||||||||
| let lastModifiedDate: Date? | ||||||||||||||||
| let creationDate: Date? | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| let reminderData = await withCheckedContinuation { (continuation: CheckedContinuation<[ReminderData], Never>) in | ||||||||||||||||
| let predicate = eventStore.predicateForReminders(in: calendars) | ||||||||||||||||
| eventStore.fetchReminders(matching: predicate) { reminders in | ||||||||||||||||
| let data = (reminders ?? []).map { reminder in | ||||||||||||||||
| ReminderData( | ||||||||||||||||
| let isAllDay = reminder.dueDateComponents?.hour == nil && reminder.dueDateComponents?.minute == nil | ||||||||||||||||
| return ReminderData( | ||||||||||||||||
| id: reminder.calendarItemIdentifier, | ||||||||||||||||
| title: reminder.title ?? "", | ||||||||||||||||
| notes: reminder.notes, | ||||||||||||||||
| isCompleted: reminder.isCompleted, | ||||||||||||||||
| completionDate: reminder.completionDate, | ||||||||||||||||
| priority: Int(reminder.priority), | ||||||||||||||||
| dueDateComponents: reminder.dueDateComponents, | ||||||||||||||||
| isAllDay: isAllDay, | ||||||||||||||||
| listID: reminder.calendar.calendarIdentifier, | ||||||||||||||||
| listName: reminder.calendar.title | ||||||||||||||||
| listName: reminder.calendar.title, | ||||||||||||||||
| lastModifiedDate: reminder.lastModifiedDate, | ||||||||||||||||
| creationDate: reminder.creationDate | ||||||||||||||||
| ) | ||||||||||||||||
| } | ||||||||||||||||
| continuation.resume(returning: data) | ||||||||||||||||
|
|
@@ -245,8 +220,11 @@ public actor RemindersStore { | |||||||||||||||
| completionDate: data.completionDate, | ||||||||||||||||
| priority: ReminderPriority(eventKitValue: data.priority), | ||||||||||||||||
| dueDate: date(from: data.dueDateComponents), | ||||||||||||||||
| isAllDay: data.isAllDay, | ||||||||||||||||
| listID: data.listID, | ||||||||||||||||
| listName: data.listName | ||||||||||||||||
| listName: data.listName, | ||||||||||||||||
| lastModifiedDate: data.lastModifiedDate, | ||||||||||||||||
| creationDate: data.creationDate | ||||||||||||||||
| ) | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -267,7 +245,13 @@ public actor RemindersStore { | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private func calendarComponents(from date: Date) -> DateComponents { | ||||||||||||||||
| calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) | ||||||||||||||||
| var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) | ||||||||||||||||
| // If the time is exactly 00:00, we treat it as an all-day (date-only) reminder | ||||||||||||||||
| if components.hour == 0 && components.minute == 0 { | ||||||||||||||||
| components.hour = nil | ||||||||||||||||
| components.minute = nil | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+248
to
+253
|
||||||||||||||||
| var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) | |
| // If the time is exactly 00:00, we treat it as an all-day (date-only) reminder | |
| if components.hour == 0 && components.minute == 0 { | |
| components.hour = nil | |
| components.minute = nil | |
| } | |
| let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) |
Copilot
AI
Mar 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same isAllDay calculation issue here: with optional chaining, a missing dueDateComponents will be treated as all-day. This can lead to inconsistent ReminderItem state (isAllDay == true but dueDate == nil). Gate the all-day check on reminder.dueDateComponents != nil first.
| let isAllDay = reminder.dueDateComponents?.hour == nil && reminder.dueDateComponents?.minute == nil | |
| let dueComponents = reminder.dueDateComponents | |
| let isAllDay = dueComponents != nil && dueComponents?.hour == nil && dueComponents?.minute == nil |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,7 +96,19 @@ enum OutputRenderer { | |
| } | ||
| for (index, reminder) in sorted.enumerated() { | ||
| let status = reminder.isCompleted ? "x" : " " | ||
| let due = reminder.dueDate.map { DateParsing.formatDisplay($0) } ?? "no due date" | ||
| let due: String | ||
| if let dueDate = reminder.dueDate { | ||
| if reminder.isAllDay { | ||
| let formatter = DateFormatter() | ||
| formatter.dateStyle = .medium | ||
| formatter.timeStyle = .none | ||
| due = formatter.string(from: dueDate) | ||
|
Comment on lines
+101
to
+105
|
||
| } else { | ||
| due = DateParsing.formatDisplay(dueDate) | ||
| } | ||
| } else { | ||
|
Comment on lines
+99
to
+109
|
||
| due = "no due date" | ||
| } | ||
| let priority = reminder.priority == .none ? "" : " priority=\(reminder.priority.rawValue)" | ||
| Swift.print("[\(index + 1)] [\(status)] \(reminder.title) [\(reminder.listName)] — \(due)\(priority)") | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isAllDayis computed asdueDateComponents?.hour == nil && ...minute == nil, which evaluates totruewhendueDateComponentsitself isnil. That will mark reminders with no due date as all-day (e.g., JSON output will showisAllDay: truewhiledueDateis null). Consider requiringdueDateComponents != nil(or checking.date/.yearpresence) before treating it as all-day.