66 lines
2.8 KiB
Swift
66 lines
2.8 KiB
Swift
import AppKit
|
|
import NucleicCore
|
|
|
|
/// Drives the "escalating alarm": while a chat needs the user and they haven't opened it,
|
|
/// repeat the "Submarine" system sound — with a short gap between plays — until they return
|
|
/// to that chat. `AppStore` tracks *which* chats are blocked-and-unviewed and calls
|
|
/// `setActive(_:)` as that set flips between empty and non-empty (`AppStore.chatAlarmHook`,
|
|
/// installed in `NucleicApp.init`); this type owns the repeating playback and honors the
|
|
/// off-by-default Settings switch. A singleton so the one alarm loop is shared app-wide.
|
|
@MainActor
|
|
final class ChatAlarm {
|
|
static let shared = ChatAlarm()
|
|
private init() {}
|
|
|
|
/// Silence between consecutive plays, so the sound repeats as a deliberate pulse rather
|
|
/// than the next copy stacking on top of the last while it's still sounding.
|
|
private let gap = Duration.seconds(0.5)
|
|
|
|
/// The running repeat loop, or nil when silent. Cancelling it stops scheduling new plays;
|
|
/// `current?.stop()` cuts off whatever is sounding right now for instant silence on return.
|
|
private var loop: Task<Void, Never>?
|
|
private var current: NSSound?
|
|
|
|
/// Start or stop the alarm. Honors the Settings switch: with the alarm turned off,
|
|
/// `active == true` is a no-op, so a blocked chat still rings only its one-shot "Pop" cue.
|
|
/// `active == false` always stops, so toggling the switch off mid-alarm silences it at once.
|
|
func setActive(_ active: Bool) {
|
|
if active && ChatStatusSounds.escalatingAlarm {
|
|
start()
|
|
} else {
|
|
stop()
|
|
}
|
|
}
|
|
|
|
private func start() {
|
|
guard loop == nil else { return } // already ringing — don't stack loops
|
|
loop = Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
// Pace each cycle by the clip's own length plus the gap, so a ~1.5s sound doesn't
|
|
// pile copies on itself; fall back to a sane length if the system can't report one.
|
|
let probe = NSSound(named: NSSound.Name(ChatStatusSounds.alarmSoundName))
|
|
let clip: Duration = (probe?.duration ?? 0) > 0 ? .seconds(probe!.duration) : .seconds(1.5)
|
|
while !Task.isCancelled {
|
|
self.ring()
|
|
try? await Task.sleep(for: clip + self.gap)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stop() {
|
|
loop?.cancel()
|
|
loop = nil
|
|
current?.stop()
|
|
current = nil
|
|
}
|
|
|
|
/// Play one fresh copy of the alarm sound, holding the reference so `stop()` can cut it off
|
|
/// the instant the user returns to the chat.
|
|
private func ring() {
|
|
guard let sound = NSSound(named: NSSound.Name(ChatStatusSounds.alarmSoundName))?.copy() as? NSSound
|
|
else { return }
|
|
current = sound
|
|
sound.play()
|
|
}
|
|
}
|