From c7da3f025c4017d26327ea569b87ee6304d2e34c Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 8 Mar 2026 18:34:19 +0530 Subject: [PATCH] HotFix: DND persistance --- DynamicIsland/ContentView.swift | 5 +-- .../managers/DoNotDisturbManager.swift | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 5c57d00..df350e4 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -75,7 +75,6 @@ struct ContentView: View { @Default(.capsLockIndicatorTintMode) var capsLockTintMode @Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection @Default(.showDoNotDisturbIndicator) var showDoNotDisturbIndicator - @Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableCapsLockIndicator) var enableCapsLockIndicator @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 { DownloadLiveActivity() .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() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed { LockScreenLiveActivity() @@ -1044,7 +1043,7 @@ struct ContentView: View { return .recording } - if enableDoNotDisturbDetection && showDoNotDisturbIndicator && (doNotDisturbManager.isDoNotDisturbActive || focusIndicatorNonPersistent) { + if enableDoNotDisturbDetection && showDoNotDisturbIndicator && doNotDisturbManager.isDoNotDisturbActive { let mode = FocusModeType.resolve(identifier: doNotDisturbManager.currentFocusModeIdentifier, name: doNotDisturbManager.currentFocusModeName) return .focus(mode) } diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index 934a0ee..518f37c 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -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. @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 metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated) 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. private var metadataClearTask: Task? + /// Task that resets `isFocusToastDismissing` after the OFF toast animation. + private var toastDismissTask: Task? + private init() { focusLogStream.onMetadataUpdate = { [weak self] identifier, name in self?.handleLogMetadataUpdate(identifier: identifier, name: name) @@ -112,6 +120,7 @@ final class DoNotDisturbManager: ObservableObject { stateVerificationTask = nil metadataClearTask?.cancel() metadataClearTask = nil + cancelFocusToastDismiss() isMonitoring = false DispatchQueue.main.async { @@ -243,10 +252,12 @@ final class DoNotDisturbManager: ObservableObject { self.currentFocusModeIdentifier = previousIdentifier self.currentFocusModeName = previousName self.scheduleMetadataClear() + self.beginFocusToastDismissIfNeeded() } 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 = nil + self.cancelFocusToastDismiss() } // Start or stop periodic verification based on new state. @@ -279,6 +290,7 @@ final class DoNotDisturbManager: ObservableObject { self.isDoNotDisturbActive = false } self.scheduleMetadataClear() + self.beginFocusToastDismissIfNeeded() debugPrint("[DoNotDisturbManager] State verification: focus no longer active, resetting.") 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?) { metadataExtractionQueue.async { [weak self] in guard let self = self else { return }