diff --git a/Sources/TerminalNotificationStore.swift b/Sources/TerminalNotificationStore.swift index b7ce2883..c3403f4f 100644 --- a/Sources/TerminalNotificationStore.swift +++ b/Sources/TerminalNotificationStore.swift @@ -42,6 +42,9 @@ enum NotificationSoundSettings { ) private static let pendingCustomSoundPreparationLock = NSLock() private static var pendingCustomSoundPreparationPaths: Set = [] + private static let activePlaybackSoundsLock = NSLock() + private static var activePlaybackSounds: [ObjectIdentifier: NSSound] = [:] + private static let activePlaybackSoundDelegate = ActivePlaybackSoundDelegate() private static let notificationSoundSupportedExtensions: Set = [ "aif", "aiff", @@ -49,6 +52,12 @@ enum NotificationSoundSettings { "wav", ] + private final class ActivePlaybackSoundDelegate: NSObject, NSSoundDelegate { + func sound(_ sound: NSSound, didFinishPlaying finishedPlaying: Bool) { + NotificationSoundSettings.releaseActivePlaybackSound(sound) + } + } + private struct CustomSoundSourceMetadata: Codable, Equatable { let sourcePath: String let sourceSize: UInt64 @@ -260,7 +269,16 @@ enum NotificationSoundSettings { playSoundFile(at: url) } + static func playSelectedSound(defaults: UserDefaults = .standard) { + let value = defaults.string(forKey: key) ?? defaultValue + playSound(value: value, defaults: defaults) + } + static func previewSound(value: String, defaults: UserDefaults = .standard) { + playSound(value: value, defaults: defaults) + } + + private static func playSound(value: String, defaults: UserDefaults) { switch value { case "default": NSSound.beep() @@ -331,10 +349,26 @@ enum NotificationSoundSettings { NSLog("Notification custom sound failed to load from path: \(url.path)") return } - sound.play() + retainActivePlaybackSound(sound) + sound.delegate = activePlaybackSoundDelegate + if !sound.play() { + releaseActivePlaybackSound(sound) + } } } + private static func retainActivePlaybackSound(_ sound: NSSound) { + activePlaybackSoundsLock.lock() + activePlaybackSounds[ObjectIdentifier(sound)] = sound + activePlaybackSoundsLock.unlock() + } + + private static func releaseActivePlaybackSound(_ sound: NSSound) { + activePlaybackSoundsLock.lock() + activePlaybackSounds.removeValue(forKey: ObjectIdentifier(sound)) + activePlaybackSoundsLock.unlock() + } + private static func cleanupStaleStagedSoundFiles( in directoryURL: URL, keeping fileName: String, @@ -694,8 +728,9 @@ final class TerminalNotificationStore: ObservableObject { store.scheduleUserNotification(notification) } private var suppressedNotificationFeedbackHandler: (TerminalNotificationStore, TerminalNotification) -> Void = { - _, - _ in + store, + notification in + store.playSuppressedNotificationFeedback(for: notification) } private var indexes = NotificationIndexes() @@ -881,7 +916,9 @@ final class TerminalNotificationStore: ObservableObject { center.removeDeliveredNotificationsOffMain(withIdentifiers: idsToClear) center.removePendingNotificationRequestsOffMain(withIdentifiers: idsToClear) } - if !shouldSuppressExternalDelivery { + if shouldSuppressExternalDelivery { + suppressedNotificationFeedbackHandler(self, notification) + } else { notificationDeliveryHandler(self, notification) } } @@ -1050,6 +1087,11 @@ final class TerminalNotificationStore: ObservableObject { } } + private func playSuppressedNotificationFeedback(for notification: TerminalNotification) { + _ = notification + NotificationSoundSettings.playSelectedSound() + } + private func ensureAuthorization( origin: AuthorizationRequestOrigin, _ completion: @escaping (Bool) -> Void @@ -1273,7 +1315,9 @@ final class TerminalNotificationStore: ObservableObject { } func resetSuppressedNotificationFeedbackHandlerForTesting() { - suppressedNotificationFeedbackHandler = { _, _ in } + suppressedNotificationFeedbackHandler = { store, notification in + store.playSuppressedNotificationFeedback(for: notification) + } } func promptToEnableNotificationsForTesting() {