HotFix: DND persistance

This commit is contained in:
Hariharan Mudaliar 2026-03-08 18:34:19 +05:30
commit c7da3f025c
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
2 changed files with 41 additions and 4 deletions

View file

@ -75,7 +75,6 @@ struct ContentView: View {
@Default(.capsLockIndicatorTintMode) var capsLockTintMode @Default(.capsLockIndicatorTintMode) var capsLockTintMode
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection @Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
@Default(.showDoNotDisturbIndicator) var showDoNotDisturbIndicator @Default(.showDoNotDisturbIndicator) var showDoNotDisturbIndicator
@Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
@Default(.enableCapsLockIndicator) var enableCapsLockIndicator @Default(.enableCapsLockIndicator) var enableCapsLockIndicator
@Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities @Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities
@ -713,7 +712,7 @@ struct ContentView: View {
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .download) && vm.notchState == .closed && downloadManager.isDownloading && Defaults[.enableDownloadListener] && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .download) && vm.notchState == .closed && downloadManager.isDownloading && Defaults[.enableDownloadListener] && !vm.hideOnClosed {
DownloadLiveActivity() DownloadLiveActivity()
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2))) .transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && (doNotDisturbManager.isDoNotDisturbActive || Defaults[.focusIndicatorNonPersistent]) && !vm.hideOnClosed && !lockScreenManager.isLocked { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && (doNotDisturbManager.isDoNotDisturbActive || doNotDisturbManager.isFocusToastDismissing) && !vm.hideOnClosed && !lockScreenManager.isLocked {
DoNotDisturbLiveActivity() DoNotDisturbLiveActivity()
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed {
LockScreenLiveActivity() LockScreenLiveActivity()
@ -1044,7 +1043,7 @@ struct ContentView: View {
return .recording return .recording
} }
if enableDoNotDisturbDetection && showDoNotDisturbIndicator && (doNotDisturbManager.isDoNotDisturbActive || focusIndicatorNonPersistent) { if enableDoNotDisturbDetection && showDoNotDisturbIndicator && doNotDisturbManager.isDoNotDisturbActive {
let mode = FocusModeType.resolve(identifier: doNotDisturbManager.currentFocusModeIdentifier, name: doNotDisturbManager.currentFocusModeName) let mode = FocusModeType.resolve(identifier: doNotDisturbManager.currentFocusModeIdentifier, name: doNotDisturbManager.currentFocusModeName)
return .focus(mode) return .focus(mode)
} }

View file

@ -33,6 +33,11 @@ final class DoNotDisturbManager: ObservableObject {
// Used by the brief-toast UI to show an ON toast when the active mode switches while Focus stays enabled. // Used by the brief-toast UI to show an ON toast when the active mode switches while Focus stays enabled.
@Published var focusToastTrigger: UUID = UUID() @Published var focusToastTrigger: UUID = UUID()
/// Briefly `true` after focus turns OFF while toast mode is enabled,
/// keeping the standalone DoNotDisturbLiveActivity mounted long enough
/// for the "Off" dismissal animation to play out.
@Published private(set) var isFocusToastDismissing: Bool = false
private let notificationCenter = DistributedNotificationCenter.default() private let notificationCenter = DistributedNotificationCenter.default()
private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated) private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated)
private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility) private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility)
@ -56,6 +61,9 @@ final class DoNotDisturbManager: ObservableObject {
/// Delayed task that clears retained metadata after the OFF animation completes. /// Delayed task that clears retained metadata after the OFF animation completes.
private var metadataClearTask: Task<Void, Never>? private var metadataClearTask: Task<Void, Never>?
/// Task that resets `isFocusToastDismissing` after the OFF toast animation.
private var toastDismissTask: Task<Void, Never>?
private init() { private init() {
focusLogStream.onMetadataUpdate = { [weak self] identifier, name in focusLogStream.onMetadataUpdate = { [weak self] identifier, name in
self?.handleLogMetadataUpdate(identifier: identifier, name: name) self?.handleLogMetadataUpdate(identifier: identifier, name: name)
@ -112,6 +120,7 @@ final class DoNotDisturbManager: ObservableObject {
stateVerificationTask = nil stateVerificationTask = nil
metadataClearTask?.cancel() metadataClearTask?.cancel()
metadataClearTask = nil metadataClearTask = nil
cancelFocusToastDismiss()
isMonitoring = false isMonitoring = false
DispatchQueue.main.async { DispatchQueue.main.async {
@ -243,10 +252,12 @@ final class DoNotDisturbManager: ObservableObject {
self.currentFocusModeIdentifier = previousIdentifier self.currentFocusModeIdentifier = previousIdentifier
self.currentFocusModeName = previousName self.currentFocusModeName = previousName
self.scheduleMetadataClear() self.scheduleMetadataClear()
self.beginFocusToastDismissIfNeeded()
} else { } else {
// Focus turned ON — cancel any pending metadata clear. // Focus turned ON — cancel any pending metadata clear and toast dismiss.
self.metadataClearTask?.cancel() self.metadataClearTask?.cancel()
self.metadataClearTask = nil self.metadataClearTask = nil
self.cancelFocusToastDismiss()
} }
// Start or stop periodic verification based on new state. // Start or stop periodic verification based on new state.
@ -279,6 +290,7 @@ final class DoNotDisturbManager: ObservableObject {
self.isDoNotDisturbActive = false self.isDoNotDisturbActive = false
} }
self.scheduleMetadataClear() self.scheduleMetadataClear()
self.beginFocusToastDismissIfNeeded()
debugPrint("[DoNotDisturbManager] State verification: focus no longer active, resetting.") debugPrint("[DoNotDisturbManager] State verification: focus no longer active, resetting.")
break break
} }
@ -321,6 +333,32 @@ final class DoNotDisturbManager: ObservableObject {
} }
} }
/// Sets `isFocusToastDismissing` to `true` for ~2.5 seconds when focus turns
/// OFF while toast mode is enabled. This keeps the standalone DND live activity
/// mounted long enough for the "Off" toast animation, without permanently
/// occupying the live-activity slot.
private func beginFocusToastDismissIfNeeded() {
guard Defaults[.focusIndicatorNonPersistent] else { return }
toastDismissTask?.cancel()
isFocusToastDismissing = true
toastDismissTask = Task { @MainActor [weak self] in
try? await Task.sleep(for: .milliseconds(2500))
guard !Task.isCancelled, let self else { return }
withAnimation(.smooth(duration: 0.25)) {
self.isFocusToastDismissing = false
}
}
}
/// Cancels any in-progress toast dismiss countdown (e.g. when focus turns back ON).
private func cancelFocusToastDismiss() {
toastDismissTask?.cancel()
toastDismissTask = nil
isFocusToastDismissing = false
}
private func handleLogMetadataUpdate(identifier: String?, name: String?) { private func handleLogMetadataUpdate(identifier: String?, name: String?) {
metadataExtractionQueue.async { [weak self] in metadataExtractionQueue.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }