From b643f1d9053e5e2117b7961a99c3adbac24e5642 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 10 Nov 2025 03:15:28 +0530 Subject: [PATCH] Working Focus mode - focus type not yet supported --- .../Focus/DoNotDisturbLiveActivity.swift | 6 + .../components/UI/FocusIndicator.swift | 11 +- .../managers/DoNotDisturbManager.swift | 111 +++++++++++++----- 3 files changed, 97 insertions(+), 31 deletions(-) diff --git a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift index aaeb179..c01aca0 100644 --- a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift +++ b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift @@ -89,10 +89,16 @@ struct DoNotDisturbLiveActivity: View { private var labelText: String { let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { + if focusMode == .doNotDisturb { + return "Focus" + } return trimmed } let fallback = focusMode.displayName + if focusMode == .doNotDisturb { + return "Focus" + } return fallback.isEmpty ? "Focus" : fallback } diff --git a/DynamicIsland/components/UI/FocusIndicator.swift b/DynamicIsland/components/UI/FocusIndicator.swift index 4bcb5e8..f887d13 100644 --- a/DynamicIsland/components/UI/FocusIndicator.swift +++ b/DynamicIsland/components/UI/FocusIndicator.swift @@ -40,8 +40,15 @@ struct FocusIndicator: View { private var accessibilityLabel: String { let trimmedName = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) - let name = trimmedName.isEmpty ? focusMode.displayName : trimmedName - return "Focus active: \(name)" + let baseName: String + if trimmedName.isEmpty { + baseName = focusMode.displayName + } else { + baseName = trimmedName + } + + let finalName = focusMode == .doNotDisturb ? "Focus" : baseName + return "Focus active: \(finalName)" } } diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index f659e93..ce3ad64 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -22,7 +22,6 @@ final class DoNotDisturbManager: ObservableObject { private let notificationCenter = DistributedNotificationCenter.default() private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated) - private var hasLoggedPayload = false private init() {} @@ -85,14 +84,17 @@ final class DoNotDisturbManager: ObservableObject { let trimmedIdentifier = metadata.identifier?.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedName = metadata.name?.trimmingCharacters(in: .whitespacesAndNewlines) - let incomingIdentifier = (trimmedIdentifier?.isEmpty == false ? trimmedIdentifier! : self.currentFocusModeIdentifier) - let incomingName = (trimmedName?.isEmpty == false ? trimmedName! : self.currentFocusModeName) + let resolvedMode = FocusModeType.resolve(identifier: trimmedIdentifier, name: trimmedName) - let resolvedMode = FocusModeType.resolve(identifier: incomingIdentifier, name: incomingName) + debugPrint("[DoNotDisturbManager] Focus update -> notification: \(notification.name.rawValue) | identifier: \(trimmedIdentifier ?? "") | name: \(trimmedName ?? "") | resolved: \(resolvedMode.rawValue)") - debugPrint("[DoNotDisturbManager] Focus update -> identifier: \(trimmedIdentifier ?? "") (incoming: \(incomingIdentifier.isEmpty ? "" : incomingIdentifier)) | name: \(trimmedName ?? "") | resolved: \(resolvedMode.rawValue)") + let finalIdentifier: String + if let identifier = trimmedIdentifier, !identifier.isEmpty { + finalIdentifier = identifier + } else { + finalIdentifier = resolvedMode.rawValue + } - let finalIdentifier = resolvedMode.rawValue if finalIdentifier != self.currentFocusModeIdentifier { self.currentFocusModeIdentifier = finalIdentifier } @@ -102,8 +104,8 @@ final class DoNotDisturbManager: ObservableObject { finalName = name } else if !resolvedMode.displayName.isEmpty { finalName = resolvedMode.displayName - } else if !incomingName.isEmpty { - finalName = incomingName + } else if let identifier = trimmedIdentifier, !identifier.isEmpty { + finalName = identifier } else { finalName = "Focus" } @@ -124,10 +126,7 @@ final class DoNotDisturbManager: ObservableObject { private func extractMetadata(from notification: Notification) -> (name: String?, identifier: String?) { guard let userInfo = notification.userInfo else { return (nil, nil) } - if !hasLoggedPayload { - hasLoggedPayload = true - debugPrint("[DoNotDisturbManager] focus notification payload: \(userInfo)") - } + debugPrint("[DoNotDisturbManager] raw focus payload -> name: \(notification.name.rawValue), object: \(String(describing: notification.object)), userInfo: \(userInfo)") let identifierKeys = [ "FocusModeIdentifier", @@ -149,15 +148,8 @@ final class DoNotDisturbManager: ObservableObject { "Name" ] - let identifier = identifierKeys - .compactMap { userInfo[$0] as? String } - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .first { !$0.isEmpty } - - let name = nameKeys - .compactMap { userInfo[$0] as? String } - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .first { !$0.isEmpty } + let identifier = firstMatch(for: identifierKeys, in: userInfo) + let name = firstMatch(for: nameKeys, in: userInfo) return (name, identifier) } @@ -186,7 +178,7 @@ enum FocusModeType: String, CaseIterable { var displayName: String { switch self { - case .doNotDisturb: return "DnD" + case .doNotDisturb: return "Do Not Disturb" case .work: return "Work" case .personal: return "Personal" case .sleep: return "Sleep" @@ -285,18 +277,79 @@ extension FocusModeType { } static func resolve(identifier: String?, name: String?) -> FocusModeType { + let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedName.isEmpty { + if let match = FocusModeType.allCases.first(where: { + guard !$0.displayName.isEmpty else { return false } + return $0.displayName.compare(trimmedName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame + }) { + return match + } + } + let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !trimmedIdentifier.isEmpty { return FocusModeType(identifier: trimmedIdentifier) } - let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if !trimmedName.isEmpty { - if let match = FocusModeType.allCases.first(where: { !$0.displayName.isEmpty && $0.displayName.compare(trimmedName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame }) { - return match - } - } - return .doNotDisturb } } + +// MARK: - Metadata helpers + +private extension DoNotDisturbManager { + func firstMatch(for keys: [String], in value: Any) -> String? { + if let dictionary = value as? [AnyHashable: Any] { + for key in keys { + if let candidate = dictionary[key], let string = normalizedString(from: candidate) { + return string + } + } + + for nestedValue in dictionary.values { + if let nestedMatch = firstMatch(for: keys, in: nestedValue) { + return nestedMatch + } + } + } else if let array = value as? [Any] { + for element in array { + if let nestedMatch = firstMatch(for: keys, in: element) { + return nestedMatch + } + } + } + + return nil + } + + func normalizedString(from value: Any) -> String? { + switch value { + case let string as String: + let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + case let number as NSNumber: + return number.stringValue + case let uuid as UUID: + return uuid.uuidString + case let uuid as NSUUID: + return uuid.uuidString + case let data as Data: + if let string = String(data: data, encoding: .utf8) { + return string.trimmingCharacters(in: .whitespacesAndNewlines) + } + return nil + case let dict as [AnyHashable: Any]: + // Attempt to pull common keys from nested dictionaries + if let nested = firstMatch(for: ["identifier", "Identifier", "uuid", "UUID"], in: dict) { + return nested + } + if let name = firstMatch(for: ["name", "Name", "displayName", "display_name"], in: dict) { + return name + } + return nil + default: + return nil + } + } +}