Browse Source

Route carbs-required through TrioAlertManager + retract when resolved

trioneer 2 weeks ago
parent
commit
76f2315199

+ 1 - 1
Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertsRootView.swift

@@ -58,7 +58,7 @@ extension GlucoseAlerts {
                 }.listRowBackground(Color.chart)
 
                 Section(footer: Text(
-                    "On by default for all CGMs that handle glucose alerts (all Dexcom CGMs, xDrip4iOS). Turn off if you've disabled those and want Trio to alert you instead."
+                    "On by default for all CGM or apps that handle glucose alerts (all Dexcom CGMs, xDrip4iOS). Turn off if you've disabled those and want Trio to alert you instead."
                 )) {
                     Toggle(isOn: Binding(
                         get: { !store.configuration.forceTrioAlertsWhenCGMProvidesOwn },

+ 4 - 0
Trio/Sources/Services/Alerts/TrioAlertCategory.swift

@@ -27,6 +27,7 @@ enum TrioAlertCategory: Equatable {
     case glucoseDataStale
     case algorithmError
     case commsTransient
+    case carbsRequired
     case other(String)
 
     /// Whether an inbound alert in this category surfaces *immediately*.
@@ -41,6 +42,7 @@ enum TrioAlertCategory: Equatable {
              .batteryEmpty,
              .batteryLow,
              .bolusFailed,
+             .carbsRequired,
              .deliveryUncertain,
              .deviceExpirationReminder,
              .deviceExpired,
@@ -90,6 +92,7 @@ enum TrioAlertCategory: Equatable {
         case .glucoseDataStale: return "glucoseDataStale"
         case .algorithmError: return "algorithmError"
         case .commsTransient: return "commsTransient"
+        case .carbsRequired: return "carbsRequired"
         case let .other(id): return id
         }
     }
@@ -107,6 +110,7 @@ enum TrioAlertCategory: Equatable {
             return .critical
         case .batteryLow,
              .bolusFailed,
+             .carbsRequired,
              .deviceExpired,
              .glucoseDataStale,
              .glucoseForecastedLow,

+ 43 - 21
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -1,6 +1,7 @@
 import Combine
 import CoreData
 import Foundation
+import LoopKit
 import SwiftUI
 import Swinject
 import UserNotifications
@@ -71,6 +72,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         broadcaster.register(alertMessageNotificationObserver.self, observer: self)
         Task { await updateGlucoseBadge() }
         configureNotificationCategories()
+        clearLegacyCarbsRequiredNotification()
         subscribeGlucoseUpdates()
     }
 
@@ -136,31 +138,48 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
     }
 
+    private static let carbsRequiredAlertID = Alert.Identifier(
+        managerIdentifier: "trio.aps",
+        alertIdentifier: TrioAlertCategory.carbsRequired.alertIdentifier
+    )
+
     private func notifyCarbsRequired(_ carbs: Int) {
         guard Decimal(carbs) >= settingsManager.settings.carbsRequiredThreshold,
-              settingsManager.settings.showCarbsRequiredBadge else { return }
-
-        var titles: [String] = []
-
-        let content = UNMutableNotificationContent()
-
-        if snoozeUntilDate > Date() {
+              settingsManager.settings.showCarbsRequiredBadge
+        else {
+            trioAlertManager.retractAlert(identifier: Self.carbsRequiredAlertID)
             return
         }
-        content.sound = .default
-
-        titles.append(String(format: String(localized: "Carbs required: %d g", comment: "Carbs required"), carbs))
-
-        content.title = titles.joined(separator: " ")
-        content.body = String(
-            format: String(
-                localized:
-                "To prevent LOW required %d g of carbs",
-                comment: "To prevent LOW required %d g of carbs"
-            ),
+        let title = String(format: String(localized: "Carbs required: %d g", comment: "Carbs required"), carbs)
+        let body = String(
+            format: String(localized: "To prevent LOW required %d g of carbs", comment: "To prevent LOW required %d g of carbs"),
             carbs
         )
-        addRequest(identifier: .carbsRequiredNotification, content: content, deleteOld: true, messageSubtype: .carb)
+        let content = Alert.Content(
+            title: title,
+            body: body,
+            acknowledgeActionButtonLabel: String(localized: "OK")
+        )
+        let alert = Alert(
+            identifier: Self.carbsRequiredAlertID,
+            foregroundContent: content,
+            backgroundContent: content,
+            trigger: .immediate,
+            interruptionLevel: TrioAlertCategory.carbsRequired.interruptionLevel
+        )
+        trioAlertManager.issueAlert(alert)
+    }
+
+    private func retractCarbsRequiredAlert() {
+        trioAlertManager.retractAlert(identifier: Self.carbsRequiredAlertID)
+    }
+
+    /// Removes any `Trio.carbsRequiredNotification` UN still sitting in the
+    /// system from a pre-pipeline install. Safe no-op when none exist.
+    private func clearLegacyCarbsRequiredNotification() {
+        let id = Identifier.carbsRequiredNotification.rawValue
+        notificationCenter.removePendingNotificationRequests(withIdentifiers: [id])
+        notificationCenter.removeDeliveredNotifications(withIdentifiers: [id])
     }
 
     private func fetchGlucoseIDs() async throws -> [NSManagedObjectID] {
@@ -330,8 +349,11 @@ extension BaseUserNotificationsManager: alertMessageNotificationObserver {
 
 extension BaseUserNotificationsManager: DeterminationObserver {
     func determinationDidUpdate(_ determination: Determination) {
-        guard let carndRequired = determination.carbsReq else { return }
-        notifyCarbsRequired(Int(carndRequired))
+        guard let carbsRequired = determination.carbsReq else {
+            retractCarbsRequiredAlert()
+            return
+        }
+        notifyCarbsRequired(Int(carbsRequired))
     }
 }