diff --git a/supacode/Clients/Notifications/NotificationSoundClient.swift b/supacode/Clients/Notifications/NotificationSoundClient.swift new file mode 100644 index 00000000..7166d5f4 --- /dev/null +++ b/supacode/Clients/Notifications/NotificationSoundClient.swift @@ -0,0 +1,33 @@ +import AppKit +import ComposableArchitecture +import Foundation + +private let appNotificationSound: NSSound? = { + guard let url = Bundle.main.url(forResource: "notification", withExtension: "wav") else { + return nil + } + return NSSound(contentsOf: url, byReference: true) +}() + +struct NotificationSoundClient { + var play: @MainActor @Sendable () -> Void +} + +extension NotificationSoundClient: DependencyKey { + static let liveValue = NotificationSoundClient( + play: { + _ = appNotificationSound?.play() + } + ) + + static let testValue = NotificationSoundClient( + play: {} + ) +} + +extension DependencyValues { + var notificationSoundClient: NotificationSoundClient { + get { self[NotificationSoundClient.self] } + set { self[NotificationSoundClient.self] = newValue } + } +} diff --git a/supacode/Features/App/Reducer/AppFeature.swift b/supacode/Features/App/Reducer/AppFeature.swift index 37c55eb9..4153b5a7 100644 --- a/supacode/Features/App/Reducer/AppFeature.swift +++ b/supacode/Features/App/Reducer/AppFeature.swift @@ -5,13 +5,6 @@ import PostHog import Sentry import SwiftUI -private let notificationSound: NSSound? = { - guard let url = Bundle.main.url(forResource: "notification", withExtension: "wav") else { - return nil - } - return NSSound(contentsOf: url, byReference: true) -}() - private enum CancelID { static let periodicRefresh = "app.periodicRefresh" } @@ -87,6 +80,7 @@ struct AppFeature { @Dependency(\.repositoryPersistence) private var repositoryPersistence @Dependency(\.workspaceClient) private var workspaceClient @Dependency(\.settingsWindowClient) private var settingsWindowClient + @Dependency(\.notificationSoundClient) private var notificationSoundClient @Dependency(\.systemNotificationClient) private var systemNotificationClient @Dependency(\.terminalClient) private var terminalClient @Dependency(\.worktreeInfoWatcher) private var worktreeInfoWatcher @@ -674,7 +668,7 @@ struct AppFeature { if state.settings.notificationSoundEnabled && !state.settings.systemNotificationsEnabled { effects.append( .run { _ in - await MainActor.run { _ = notificationSound?.play() } + await notificationSoundClient.play() } ) } diff --git a/supacodeTests/AppFeatureSystemNotificationTests.swift b/supacodeTests/AppFeatureSystemNotificationTests.swift index 51aedf5e..f3e5f7db 100644 --- a/supacodeTests/AppFeatureSystemNotificationTests.swift +++ b/supacodeTests/AppFeatureSystemNotificationTests.swift @@ -135,4 +135,73 @@ struct AppFeatureSystemNotificationTests { #expect(sends.value == [("Done", "Build succeeded")]) } + + @Test(.dependencies) func notificationReceivedSkipsLocalSoundWhenSystemNotificationsEnabled() async { + var globalSettings = GlobalSettings.default + globalSettings.systemNotificationsEnabled = true + globalSettings.notificationSoundEnabled = true + let plays = LockIsolated(0) + let store = TestStore( + initialState: AppFeature.State( + settings: SettingsFeature.State(settings: globalSettings) + ) + ) { + AppFeature() + } withDependencies: { + $0.notificationSoundClient.play = { + plays.withValue { $0 += 1 } + } + } + store.exhaustivity = .off + + await store.send( + .terminalEvent( + .notificationReceived( + worktreeID: "/tmp/repo/wt-1", + title: "Done", + body: "Build succeeded" + ) + ) + ) + await store.finish() + + #expect(plays.value == 0) + } + + @Test(.dependencies) func notificationReceivedPlaysLocalSoundWhenSystemNotificationsDisabled() async { + var globalSettings = GlobalSettings.default + globalSettings.systemNotificationsEnabled = false + globalSettings.notificationSoundEnabled = true + let plays = LockIsolated(0) + let sends = LockIsolated(0) + let store = TestStore( + initialState: AppFeature.State( + settings: SettingsFeature.State(settings: globalSettings) + ) + ) { + AppFeature() + } withDependencies: { + $0.notificationSoundClient.play = { + plays.withValue { $0 += 1 } + } + $0.systemNotificationClient.send = { _, _ in + sends.withValue { $0 += 1 } + } + } + store.exhaustivity = .off + + await store.send( + .terminalEvent( + .notificationReceived( + worktreeID: "/tmp/repo/wt-1", + title: "Done", + body: "Build succeeded" + ) + ) + ) + await store.finish() + + #expect(plays.value == 1) + #expect(sends.value == 0) + } }