Skip to content

Commit 4c9cd02

Browse files
committed
Add due date notifications
1 parent 9496542 commit 4c9cd02

File tree

9 files changed

+692
-4
lines changed

9 files changed

+692
-4
lines changed

TaskMenu.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
11E28771324DA02B74C9983F /* DateFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500B536CAEBC7F3B0AF40519 /* DateFormattingTests.swift */; };
1414
3059000A80728B12B6989398 /* KeychainServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058C00FB539FD9B1D05BCBFD /* KeychainServiceTests.swift */; };
1515
423DB4F7DE968CBD6A6E805D /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BD10764723731B64E9E183 /* AppState.swift */; };
16+
44A6F4A2E806FF1D44F0E19B /* TestDueDateNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADED99C71B9C470D7D37974C /* TestDueDateNotificationService.swift */; };
1617
48943AA6EDE34BA7B373106E /* QuickAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01BFC3843C941C89C85576B /* QuickAddView.swift */; };
1718
4C4A40DBCD2948E2FC0F1F3A /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7BCF6CD54266E26B32F6DD /* DateFormatting.swift */; };
1819
5D77ED33F50BA2FB8E0F4B8F /* MenuBarPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EAB07F42BE73AAED7BCC56 /* MenuBarPopover.swift */; };
@@ -25,11 +26,13 @@
2526
9ADC495EEB87994A08F73AB8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12212B7B62ECCDD28979CA54 /* Constants.swift */; };
2627
9BE1233CADF8C3BFFC734F1D /* ListPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A7998A82BE9D4729E85B13 /* ListPickerView.swift */; };
2728
B0420689B1657699950213A7 /* InMemoryKeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE9F6BF8DFE197A82CE6A11 /* InMemoryKeychainService.swift */; };
29+
B8593A4B6220752FE803BAF3 /* DueDateNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39705E088F29F844C149617 /* DueDateNotificationService.swift */; };
2830
C169507407838BDD4C665CF1 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFA7FEDB3F9559D6C71C2C4 /* KeychainService.swift */; };
2931
C1AE79D6748EFE551C7E8F1B /* TaskMenuApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF69FFA11009E78484C090A /* TaskMenuApp.swift */; };
3032
C1D9C85DFAE57179E9A438A1 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8F4164166B2474EB88443EC /* TaskRowView.swift */; };
3133
CD58A0F2F651970AF75746B2 /* GlobalShortcutMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF02C27B9BF06291480BD944 /* GlobalShortcutMonitor.swift */; };
3234
D321CE95DA3DC0D6090CD619 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB598B8D34C0A014D7826A63 /* MockURLProtocol.swift */; };
35+
D75722B06B77536D032C2FCD /* DueDateNotificationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE4010667E3A28546894589 /* DueDateNotificationServiceTests.swift */; };
3336
DB500BD90BD49B8D4ECBDA15 /* TaskDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED98B3B1A1223A88415EB92 /* TaskDetailView.swift */; };
3437
DE51E84794E46D07C1401DE6 /* TaskList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633899C51990EF71D24F5C89 /* TaskList.swift */; };
3538
DFD21FE92BBBBD402B0B05A1 /* AppStateBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0C7E5843F31C5163A17D3D /* AppStateBehaviorTests.swift */; };
@@ -72,9 +75,12 @@
7275
7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
7376
8F3614C7335BB0200D28F074 /* AppStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTests.swift; sourceTree = "<group>"; };
7477
8FE1F31A1E2F87FD9042F1B4 /* GoogleTasksAPIBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleTasksAPIBehaviorTests.swift; sourceTree = "<group>"; };
78+
9FE4010667E3A28546894589 /* DueDateNotificationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DueDateNotificationServiceTests.swift; sourceTree = "<group>"; };
7579
A8F4164166B2474EB88443EC /* TaskRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowView.swift; sourceTree = "<group>"; };
80+
ADED99C71B9C470D7D37974C /* TestDueDateNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDueDateNotificationService.swift; sourceTree = "<group>"; };
7681
AF02C27B9BF06291480BD944 /* GlobalShortcutMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalShortcutMonitor.swift; sourceTree = "<group>"; };
7782
B01BFC3843C941C89C85576B /* QuickAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickAddView.swift; sourceTree = "<group>"; };
83+
B39705E088F29F844C149617 /* DueDateNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DueDateNotificationService.swift; sourceTree = "<group>"; };
7884
BB598B8D34C0A014D7826A63 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = "<group>"; };
7985
BFE9F6BF8DFE197A82CE6A11 /* InMemoryKeychainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryKeychainService.swift; sourceTree = "<group>"; };
8086
C3377E75BA19027E464EAEB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -119,6 +125,7 @@
119125
2A93E24654445FC8029C75F7 /* Services */ = {
120126
isa = PBXGroup;
121127
children = (
128+
B39705E088F29F844C149617 /* DueDateNotificationService.swift */,
122129
AF02C27B9BF06291480BD944 /* GlobalShortcutMonitor.swift */,
123130
63E868E0B0520F6ABD40D350 /* GoogleAuthService.swift */,
124131
4DC959FBC4EF3DD0EF5D14D6 /* GoogleTasksAPI.swift */,
@@ -180,13 +187,15 @@
180187
1C0C7E5843F31C5163A17D3D /* AppStateBehaviorTests.swift */,
181188
8F3614C7335BB0200D28F074 /* AppStateTests.swift */,
182189
500B536CAEBC7F3B0AF40519 /* DateFormattingTests.swift */,
190+
9FE4010667E3A28546894589 /* DueDateNotificationServiceTests.swift */,
183191
C5360741190B5DF2436BE448 /* GoogleAuthServiceTests.swift */,
184192
8FE1F31A1E2F87FD9042F1B4 /* GoogleTasksAPIBehaviorTests.swift */,
185193
C79B44669D075555352A0BB4 /* GoogleTasksAPITests.swift */,
186194
BFE9F6BF8DFE197A82CE6A11 /* InMemoryKeychainService.swift */,
187195
058C00FB539FD9B1D05BCBFD /* KeychainServiceTests.swift */,
188196
BB598B8D34C0A014D7826A63 /* MockURLProtocol.swift */,
189197
6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */,
198+
ADED99C71B9C470D7D37974C /* TestDueDateNotificationService.swift */,
190199
347E0B7F9B165D9C10450722 /* TestGlobalShortcutMonitor.swift */,
191200
);
192201
path = TaskMenuTests;
@@ -299,6 +308,7 @@
299308
423DB4F7DE968CBD6A6E805D /* AppState.swift in Sources */,
300309
9ADC495EEB87994A08F73AB8 /* Constants.swift in Sources */,
301310
4C4A40DBCD2948E2FC0F1F3A /* DateFormatting.swift in Sources */,
311+
B8593A4B6220752FE803BAF3 /* DueDateNotificationService.swift in Sources */,
302312
CD58A0F2F651970AF75746B2 /* GlobalShortcutMonitor.swift in Sources */,
303313
0A9AE96BCBF5513327268CAD /* GoogleAuthService.swift in Sources */,
304314
76E308FD16F5E910DE7D81E6 /* GoogleTasksAPI.swift in Sources */,
@@ -324,13 +334,15 @@
324334
DFD21FE92BBBBD402B0B05A1 /* AppStateBehaviorTests.swift in Sources */,
325335
FEBB3BC550B3CC63B620ED8F /* AppStateTests.swift in Sources */,
326336
11E28771324DA02B74C9983F /* DateFormattingTests.swift in Sources */,
337+
D75722B06B77536D032C2FCD /* DueDateNotificationServiceTests.swift in Sources */,
327338
986954B711E286DA6CDC3353 /* GoogleAuthServiceTests.swift in Sources */,
328339
068F90845303418F7954D22E /* GoogleTasksAPIBehaviorTests.swift in Sources */,
329340
69765D5311C642C2DFADB0A0 /* GoogleTasksAPITests.swift in Sources */,
330341
B0420689B1657699950213A7 /* InMemoryKeychainService.swift in Sources */,
331342
3059000A80728B12B6989398 /* KeychainServiceTests.swift in Sources */,
332343
D321CE95DA3DC0D6090CD619 /* MockURLProtocol.swift in Sources */,
333344
0FD718B29CF8183561A6F550 /* TaskItemModelTests.swift in Sources */,
345+
44A6F4A2E806FF1D44F0E19B /* TestDueDateNotificationService.swift in Sources */,
334346
7701A8FF61E651659F4776CB /* TestGlobalShortcutMonitor.swift in Sources */,
335347
);
336348
runOnlyForDeploymentPostprocessing = 0;

TaskMenu/Models/AppState.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ final class AppState {
2020
shortcutMonitor.setEnabled(globalShortcutEnabled)
2121
}
2222
}
23+
var dueDateNotificationsEnabled: Bool {
24+
didSet {
25+
userDefaults.set(
26+
dueDateNotificationsEnabled,
27+
forKey: Constants.UserDefaults.dueDateNotificationsEnabledKey
28+
)
29+
let enabled = dueDateNotificationsEnabled
30+
Task { [weak self] in
31+
guard let self else { return }
32+
await self.applyDueDateNotificationsPreferenceChange(enabled: enabled)
33+
}
34+
}
35+
}
2336

2437
var selectedList: TaskList? {
2538
taskLists.first { $0.id == selectedListId }
@@ -29,6 +42,7 @@ final class AppState {
2942
private let api: GoogleTasksAPI
3043
private let userDefaults: UserDefaults
3144
private let shortcutMonitor: GlobalShortcutMonitoring
45+
private let dueDateNotificationService: any DueDateNotificationServicing
3246

3347
/// Whether completed tasks have been fetched for the current list
3448
private var completedTasksFetched = false
@@ -39,15 +53,20 @@ final class AppState {
3953
authService: GoogleAuthService = GoogleAuthService(),
4054
api: GoogleTasksAPI? = nil,
4155
userDefaults: UserDefaults = .standard,
42-
shortcutMonitor: GlobalShortcutMonitoring? = nil
56+
shortcutMonitor: GlobalShortcutMonitoring? = nil,
57+
dueDateNotificationService: any DueDateNotificationServicing = DueDateNotificationService()
4358
) {
4459
self.authService = authService
4560
self.api = api ?? GoogleTasksAPI(authService: authService)
4661
self.userDefaults = userDefaults
4762
self.shortcutMonitor = shortcutMonitor ?? GlobalShortcutMonitor()
63+
self.dueDateNotificationService = dueDateNotificationService
4864
self.globalShortcutEnabled = userDefaults.object(
4965
forKey: Constants.UserDefaults.globalShortcutEnabledKey
5066
) as? Bool ?? true
67+
self.dueDateNotificationsEnabled = userDefaults.object(
68+
forKey: Constants.UserDefaults.dueDateNotificationsEnabledKey
69+
) as? Bool ?? true
5170
self.isSignedIn = authService.isSignedIn
5271

5372
self.shortcutMonitor.setHandler { [weak self] in
@@ -86,6 +105,10 @@ final class AppState {
86105
selectedListId = nil
87106
completedTasksFetched = false
88107
completedTasksCache = []
108+
let dueDateNotificationService = dueDateNotificationService
109+
Task {
110+
await dueDateNotificationService.removeAllNotifications()
111+
}
89112
}
90113

91114
func loadTaskLists() async {
@@ -118,6 +141,7 @@ final class AppState {
118141
completedTasksFetched = true
119142
tasks = activeTasks + completed
120143
}
144+
await syncDueDateNotificationsIfNeeded()
121145
} catch {
122146
handleError(error)
123147
}
@@ -133,6 +157,7 @@ final class AppState {
133157
completedTasksCache = allTasks.filter { $0.isCompleted }
134158
completedTasksFetched = true
135159
tasks = allTasks
160+
await syncDueDateNotificationsIfNeeded()
136161
} catch {
137162
handleError(error)
138163
}
@@ -143,6 +168,7 @@ final class AppState {
143168
do {
144169
let task = try await api.createTask(listId: listId, title: title)
145170
tasks.insert(task, at: 0)
171+
await syncDueDateNotificationsIfNeeded()
146172
} catch {
147173
handleError(error)
148174
}
@@ -172,6 +198,7 @@ final class AppState {
172198
} else {
173199
completedTasksCache.removeAll { $0.id == result.id }
174200
}
201+
await syncDueDateNotificationsIfNeeded()
175202
} catch {
176203
// Revert optimistic update on failure
177204
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
@@ -194,6 +221,7 @@ final class AppState {
194221
completedTasksCache[idx] = result
195222
}
196223
}
224+
await syncDueDateNotificationsIfNeeded()
197225
} catch {
198226
handleError(error)
199227
}
@@ -205,6 +233,10 @@ final class AppState {
205233
try await api.deleteTask(listId: listId, taskId: task.id)
206234
tasks.removeAll { $0.id == task.id }
207235
completedTasksCache.removeAll { $0.id == task.id }
236+
await dueDateNotificationService.removeNotifications(
237+
forTaskIDs: [task.id],
238+
inListID: listId
239+
)
208240
} catch {
209241
handleError(error)
210242
}
@@ -235,6 +267,19 @@ final class AppState {
235267
}
236268
}
237269

270+
private func applyDueDateNotificationsPreferenceChange(enabled: Bool) async {
271+
if enabled {
272+
await syncDueDateNotificationsIfNeeded()
273+
} else {
274+
await dueDateNotificationService.removeAllNotifications()
275+
}
276+
}
277+
278+
private func syncDueDateNotificationsIfNeeded() async {
279+
guard dueDateNotificationsEnabled, let selectedList else { return }
280+
await dueDateNotificationService.syncNotifications(for: tasks, in: selectedList)
281+
}
282+
238283
private func toggleMenuBarPopover() {
239284
let wasVisible = isMenuBarPopoverVisible
240285

0 commit comments

Comments
 (0)