@@ -23,16 +23,40 @@ final class NotificationManager: NSObject, ObservableObject {
23
23
@Published private( set) var notifications : [ CENotification ] = [ ]
24
24
25
25
/// Currently displayed notifications in the overlay
26
- @Published private( set) var activeNotification : CENotification ?
27
26
@Published private( set) var activeNotifications : [ CENotification ] = [ ]
28
27
29
28
private var timers : [ UUID : Timer ] = [ : ]
30
29
private let displayDuration : TimeInterval = 5.0
31
30
private var isPaused : Bool = false
32
31
private var isAppActive : Bool = true
33
- private var hiddenStickyNotifications : [ CENotification ] = [ ]
34
- private var hiddenNonStickyNotifications : [ CENotification ] = [ ]
35
- private var dismissedNotificationIds : Set < UUID > = [ ] // Track dismissed notifications
32
+
33
+ /// Whether notifications were manually shown via toolbar
34
+ @Published private( set) var isManuallyShown : Bool = false
35
+
36
+ /// Set of hidden notification IDs
37
+ private var hiddenNotificationIds : Set < UUID > = [ ]
38
+
39
+ /// Whether any non-sticky notifications are currently hidden
40
+ private var hasHiddenNotifications : Bool {
41
+ activeNotifications. contains { notification in
42
+ !notification. isSticky && !isNotificationVisible( notification)
43
+ }
44
+ }
45
+
46
+ /// Whether a notification should be visible in the overlay
47
+ func isNotificationVisible( _ notification: CENotification ) -> Bool {
48
+ if notification. isBeingDismissed {
49
+ return true // Always show notifications being dismissed
50
+ }
51
+ if notification. isSticky {
52
+ return true // Always show sticky notifications
53
+ }
54
+ if isManuallyShown {
55
+ return true // Show all notifications when manually shown
56
+ }
57
+ // Otherwise, show if not hidden and has active timer
58
+ return !hiddenNotificationIds. contains ( notification. id) && timers [ notification. id] != nil
59
+ }
36
60
37
61
override private init ( ) {
38
62
super. init ( )
@@ -61,6 +85,16 @@ final class NotificationManager: NSObject, ObservableObject {
61
85
@objc
62
86
private func applicationDidBecomeActive( ) {
63
87
isAppActive = true
88
+
89
+ // Show any pending notifications in the overlay
90
+ notifications
91
+ . filter { notification in
92
+ // Only show notifications that aren't already in the overlay
93
+ !activeNotifications. contains { $0. id == notification. id }
94
+ }
95
+ . forEach { notification in
96
+ showTemporaryNotification ( notification)
97
+ }
64
98
}
65
99
66
100
@objc
@@ -103,7 +137,8 @@ final class NotificationManager: NSObject, ObservableObject {
103
137
description: description,
104
138
actionButtonTitle: actionButtonTitle,
105
139
action: action,
106
- isSticky: isSticky
140
+ isSticky: isSticky,
141
+ isRead: false // Always start as unread
107
142
)
108
143
109
144
DispatchQueue . main. async { [ weak self] in
@@ -213,11 +248,37 @@ final class NotificationManager: NSObject, ObservableObject {
213
248
214
249
/// Shows a notification in the app's overlay UI
215
250
private func showTemporaryNotification( _ notification: CENotification ) {
216
- activeNotifications. insert ( notification, at: 0 ) // Add to start of array
217
-
218
- guard !notification. isSticky else { return }
251
+ withAnimation ( . easeInOut( duration: 0.3 ) ) {
252
+ insertNotification ( notification)
253
+ hiddenNotificationIds. remove ( notification. id) // Ensure new notification is visible
254
+ // Only start timer if notifications aren't manually shown
255
+ if !isManuallyShown && !notification. isSticky {
256
+ startHideTimer ( for: notification)
257
+ }
258
+ }
259
+ }
219
260
220
- startHideTimer ( for: notification)
261
+ /// Inserts a notification in the correct position (sticky notifications on top)
262
+ private func insertNotification( _ notification: CENotification ) {
263
+ if notification. isSticky {
264
+ // Find the first sticky notification (to insert before it)
265
+ if let firstStickyIndex = activeNotifications. firstIndex ( where: { $0. isSticky } ) {
266
+ // Insert at the very start of sticky group
267
+ activeNotifications. insert ( notification, at: firstStickyIndex)
268
+ } else {
269
+ // No sticky notifications yet, insert at the start
270
+ activeNotifications. insert ( notification, at: 0 )
271
+ }
272
+ } else {
273
+ // Find the first non-sticky notification
274
+ if let firstNonStickyIndex = activeNotifications. firstIndex ( where: { !$0. isSticky } ) {
275
+ // Insert at the start of non-sticky group
276
+ activeNotifications. insert ( notification, at: firstNonStickyIndex)
277
+ } else {
278
+ // No non-sticky notifications yet, append at the end
279
+ activeNotifications. append ( notification)
280
+ }
281
+ }
221
282
}
222
283
223
284
/// Starts the timer to automatically hide a non-sticky notification
@@ -231,7 +292,14 @@ final class NotificationManager: NSObject, ObservableObject {
231
292
withTimeInterval: displayDuration,
232
293
repeats: false
233
294
) { [ weak self] _ in
234
- self ? . hideNotification ( notification)
295
+ guard let self = self else { return }
296
+ self . timers [ notification. id] = nil
297
+
298
+ withAnimation ( . easeInOut( duration: 0.3 ) ) {
299
+ // Hide this specific notification
300
+ self . hiddenNotificationIds. insert ( notification. id)
301
+ self . objectWillChange. send ( )
302
+ }
235
303
}
236
304
}
237
305
@@ -244,23 +312,29 @@ final class NotificationManager: NSObject, ObservableObject {
244
312
/// Resumes all auto-hide timers
245
313
func resumeTimer( ) {
246
314
isPaused = false
315
+ // Only restart timers for notifications that are currently visible
247
316
activeNotifications
248
- . filter { !$0. isSticky }
317
+ . filter { !$0. isSticky && isNotificationVisible ( $0 ) }
249
318
. forEach { startHideTimer ( for: $0) }
250
319
}
251
320
252
- /// Hides a specific notification
253
- private func hideNotification( _ notification: CENotification ) {
254
- timers [ notification. id] ? . invalidate ( )
255
- timers [ notification. id] = nil
256
- activeNotifications. removeAll ( where: { $0. id == notification. id } )
257
- }
258
-
259
321
/// Dismisses a specific notification
260
322
func dismissNotification( _ notification: CENotification ) {
261
- hideNotification ( notification)
262
- dismissedNotificationIds. insert ( notification. id) // Track dismissed notification
323
+ timers [ notification. id] ? . invalidate ( )
324
+ timers [ notification. id] = nil
325
+ hiddenNotificationIds. remove ( notification. id)
326
+
327
+ if let index = activeNotifications. firstIndex ( where: { $0. id == notification. id } ) {
328
+ activeNotifications [ index] . isBeingDismissed = true
329
+ }
330
+
331
+ withAnimation ( . easeOut( duration: 0.2 ) ) {
332
+ activeNotifications. removeAll ( where: { $0. id == notification. id } )
333
+ }
263
334
notifications. removeAll ( where: { $0. id == notification. id } )
335
+
336
+ // Mark as read when dismissed
337
+ markAsRead ( notification)
264
338
}
265
339
266
340
/// Marks a notification as read
@@ -281,21 +355,22 @@ final class NotificationManager: NSObject, ObservableObject {
281
355
}
282
356
}
283
357
284
- /// Hides all notifications from the overlay view
285
- func hideOverlayNotifications( ) {
286
- dismissedNotificationIds. removeAll ( ) // Clear dismissed tracking when hiding
287
- hiddenStickyNotifications = activeNotifications. filter { $0. isSticky }
288
- hiddenNonStickyNotifications = activeNotifications. filter { !$0. isSticky }
289
- activeNotifications. removeAll ( )
290
- }
291
-
292
- /// Restores only sticky notifications to the overlay
293
- func restoreOverlayStickies( ) {
294
- // Only restore sticky notifications that weren't dismissed
295
- let nonDismissedStickies = hiddenStickyNotifications. filter { !dismissedNotificationIds. contains ( $0. id) }
296
- activeNotifications. insert ( contentsOf: nonDismissedStickies, at: 0 )
297
- hiddenStickyNotifications. removeAll ( )
298
- dismissedNotificationIds. removeAll ( ) // Clear tracking after restore
358
+ /// Toggles visibility of notifications in the overlay
359
+ func toggleNotificationsVisibility( ) {
360
+ withAnimation ( . easeInOut( duration: 0.3 ) ) {
361
+ if hasHiddenNotifications || !isManuallyShown {
362
+ // Show all notifications
363
+ isManuallyShown = true
364
+ hiddenNotificationIds. removeAll ( ) // Clear all hidden states
365
+ } else {
366
+ // Hide all non-sticky notifications
367
+ isManuallyShown = false
368
+ activeNotifications
369
+ . filter { !$0. isSticky }
370
+ . forEach { hiddenNotificationIds. insert ( $0. id) }
371
+ }
372
+ objectWillChange. send ( )
373
+ }
299
374
}
300
375
}
301
376
0 commit comments