@@ -23,16 +23,40 @@ final class NotificationManager: NSObject, ObservableObject {
2323 @Published private( set) var notifications : [ CENotification ] = [ ]
2424
2525 /// Currently displayed notifications in the overlay
26- @Published private( set) var activeNotification : CENotification ?
2726 @Published private( set) var activeNotifications : [ CENotification ] = [ ]
2827
2928 private var timers : [ UUID : Timer ] = [ : ]
3029 private let displayDuration : TimeInterval = 5.0
3130 private var isPaused : Bool = false
3231 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+ }
3660
3761 override private init ( ) {
3862 super. init ( )
@@ -61,6 +85,16 @@ final class NotificationManager: NSObject, ObservableObject {
6185 @objc
6286 private func applicationDidBecomeActive( ) {
6387 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+ }
6498 }
6599
66100 @objc
@@ -103,7 +137,8 @@ final class NotificationManager: NSObject, ObservableObject {
103137 description: description,
104138 actionButtonTitle: actionButtonTitle,
105139 action: action,
106- isSticky: isSticky
140+ isSticky: isSticky,
141+ isRead: false // Always start as unread
107142 )
108143
109144 DispatchQueue . main. async { [ weak self] in
@@ -213,11 +248,37 @@ final class NotificationManager: NSObject, ObservableObject {
213248
214249 /// Shows a notification in the app's overlay UI
215250 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+ }
219260
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+ }
221282 }
222283
223284 /// Starts the timer to automatically hide a non-sticky notification
@@ -231,7 +292,14 @@ final class NotificationManager: NSObject, ObservableObject {
231292 withTimeInterval: displayDuration,
232293 repeats: false
233294 ) { [ 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+ }
235303 }
236304 }
237305
@@ -244,23 +312,29 @@ final class NotificationManager: NSObject, ObservableObject {
244312 /// Resumes all auto-hide timers
245313 func resumeTimer( ) {
246314 isPaused = false
315+ // Only restart timers for notifications that are currently visible
247316 activeNotifications
248- . filter { !$0. isSticky }
317+ . filter { !$0. isSticky && isNotificationVisible ( $0 ) }
249318 . forEach { startHideTimer ( for: $0) }
250319 }
251320
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-
259321 /// Dismisses a specific notification
260322 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+ }
263334 notifications. removeAll ( where: { $0. id == notification. id } )
335+
336+ // Mark as read when dismissed
337+ markAsRead ( notification)
264338 }
265339
266340 /// Marks a notification as read
@@ -281,21 +355,22 @@ final class NotificationManager: NSObject, ObservableObject {
281355 }
282356 }
283357
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+ }
299374 }
300375}
301376
0 commit comments